From 67510128465d63172f51edd1d5b494eed85c0a8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:24:46 +0000 Subject: [PATCH 1/6] Initial plan From a4422f772d315e1544e813e85211f91d58a01fe1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:29:43 +0000 Subject: [PATCH 2/6] Add launchd service configuration and orchestrator daemon support Co-authored-by: aurelianware <194855645+aurelianware@users.noreply.github.com> --- LAUNCHD_SERVICE_GUIDE.md | 426 ++++++++++++++++++++++++ com.privaseeai.security.plist | 64 ++++ src/privaseeai_security/daemon.py | 90 +++++ src/privaseeai_security/orchestrator.py | 57 ++++ 4 files changed, 637 insertions(+) create mode 100644 LAUNCHD_SERVICE_GUIDE.md create mode 100644 com.privaseeai.security.plist create mode 100644 src/privaseeai_security/daemon.py diff --git a/LAUNCHD_SERVICE_GUIDE.md b/LAUNCHD_SERVICE_GUIDE.md new file mode 100644 index 0000000..23ef300 --- /dev/null +++ b/LAUNCHD_SERVICE_GUIDE.md @@ -0,0 +1,426 @@ +# PrivaseeAI Security - launchd Service Installation Guide + +This guide explains how to install, configure, and manage the PrivaseeAI Security background service using macOS launchd. + +## Overview + +The `com.privaseeai.security.plist` file configures PrivaseeAI Security to run as a persistent background service that: + +- ✅ Runs `python -m privaseeai_security.orchestrator` automatically +- ✅ Starts on boot (user agent) +- ✅ Auto-restarts if crashed (with exponential backoff via ThrottleInterval) +- ✅ Logs to `/var/log/privaseeai/security.log` +- ✅ Runs from `/opt/privaseeai` working directory +- ✅ Executes as the current user +- ✅ Prevents rapid restart loops with 60-second throttle + +## Prerequisites + +1. **Python 3.11+** installed at `/usr/bin/python3` +2. **PrivaseeAI Security** installed as a Python package +3. **Proper permissions** to write to `/var/log/privaseeai/` + +## Installation Steps + +### 1. Install PrivaseeAI Security Package + +First, ensure the package is installed in your Python environment: + +```bash +# From the repository root +pip install -e . + +# Verify installation +python3 -m privaseeai_security --version +``` + +### 2. Create Required Directories + +Create the log directory with proper permissions: + +```bash +# Create log directory +sudo mkdir -p /var/log/privaseeai + +# Set ownership to current user +sudo chown $(whoami):staff /var/log/privaseeai + +# Set appropriate permissions +chmod 755 /var/log/privaseeai +``` + +### 3. Set Up Working Directory + +Create and configure the working directory: + +```bash +# Create working directory +sudo mkdir -p /opt/privaseeai + +# Set ownership to current user +sudo chown $(whoami):staff /opt/privaseeai + +# Copy or link your configuration files +# (Optional - adjust based on your setup) +# cp config.yaml /opt/privaseeai/ +``` + +### 4. Configure Environment Variables (Optional) + +If you need Telegram alerts, set environment variables in the plist or create a configuration file: + +```bash +# Option 1: Edit the plist file to add your tokens +# Edit com.privaseeai.security.plist and add to EnvironmentVariables: +# TELEGRAM_BOT_TOKEN +# your_bot_token_here +# TELEGRAM_CHAT_ID +# your_chat_id_here + +# Option 2: Use a .env file in /opt/privaseeai/ +cat > /opt/privaseeai/.env << 'EOF' +TELEGRAM_BOT_TOKEN=your_bot_token_here +TELEGRAM_CHAT_ID=your_chat_id_here +EOF +chmod 600 /opt/privaseeai/.env +``` + +### 5. Install the launchd Service + +Copy the plist file to the user's LaunchAgents directory: + +```bash +# Copy plist to LaunchAgents +cp com.privaseeai.security.plist ~/Library/LaunchAgents/ + +# Verify the file is in place +ls -l ~/Library/LaunchAgents/com.privaseeai.security.plist +``` + +## Loading and Managing the Service + +### Load the Service + +Start the service immediately and enable auto-start on boot: + +```bash +# Load and start the service +launchctl load ~/Library/LaunchAgents/com.privaseeai.security.plist + +# Alternative: Bootstrap (recommended on macOS 11+) +launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.privaseeai.security.plist +``` + +### Check Service Status + +Verify the service is running: + +```bash +# List loaded services and check if ours is running +launchctl list | grep com.privaseeai.security + +# Get detailed status +launchctl print gui/$(id -u)/com.privaseeai.security +``` + +Expected output for running service: +``` +12345 0 com.privaseeai.security +``` +(PID, exit code, label) + +### View Service Logs + +Monitor the service logs in real-time: + +```bash +# Tail the log file +tail -f /var/log/privaseeai/security.log + +# View last 100 lines +tail -n 100 /var/log/privaseeai/security.log + +# Search for errors +grep -i error /var/log/privaseeai/security.log +``` + +### Restart the Service + +If you need to restart the service: + +```bash +# Stop the service +launchctl stop com.privaseeai.security + +# Start the service (it will auto-restart due to KeepAlive) +launchctl start com.privaseeai.security + +# Or unload and reload +launchctl unload ~/Library/LaunchAgents/com.privaseeai.security.plist +launchctl load ~/Library/LaunchAgents/com.privaseeai.security.plist +``` + +### Stop and Unload the Service + +To completely stop and disable the service: + +```bash +# Unload the service (stops it and disables auto-start) +launchctl unload ~/Library/LaunchAgents/com.privaseeai.security.plist + +# Alternative: Bootout (recommended on macOS 11+) +launchctl bootout gui/$(id -u)/com.privaseeai.security +``` + +## Testing the Service + +### Basic Functionality Test + +1. **Load the service:** + ```bash + launchctl load ~/Library/LaunchAgents/com.privaseeai.security.plist + ``` + +2. **Check if it's running:** + ```bash + launchctl list | grep com.privaseeai.security + ps aux | grep "privaseeai_security.orchestrator" + ``` + +3. **Verify logs are being written:** + ```bash + tail -f /var/log/privaseeai/security.log + ``` + + You should see startup messages like: + ``` + INFO - Starting PrivaseeAI Security Orchestrator daemon... + INFO - ✅ Orchestrator daemon started + ``` + +### Crash Recovery Test + +Test the auto-restart functionality: + +1. **Find the service PID:** + ```bash + launchctl list | grep com.privaseeai.security + # Note the PID (first column) + ``` + +2. **Kill the process:** + ```bash + kill -9 + ``` + +3. **Wait 60 seconds (ThrottleInterval), then check:** + ```bash + launchctl list | grep com.privaseeai.security + ``` + + The service should have a new PID, indicating it restarted automatically. + +4. **Check logs for restart:** + ```bash + grep -A 5 "Orchestrator daemon started" /var/log/privaseeai/security.log | tail -20 + ``` + +### Boot Persistence Test + +Test auto-start on boot: + +1. **Reboot your system** +2. **After login, check service status:** + ```bash + launchctl list | grep com.privaseeai.security + tail /var/log/privaseeai/security.log + ``` + +### Manual Invocation Test + +Test running the orchestrator manually (without launchd): + +```bash +# Run directly +cd /opt/privaseeai +python3 -m privaseeai_security.orchestrator + +# Should start and show logs in terminal +# Press Ctrl+C to stop +``` + +## Troubleshooting + +### Service Won't Start + +1. **Check Python installation:** + ```bash + which python3 + /usr/bin/python3 --version # Should be 3.11+ + ``` + +2. **Verify package installation:** + ```bash + python3 -c "import privaseeai_security; print(privaseeai_security.__version__)" + ``` + +3. **Check permissions:** + ```bash + ls -la /var/log/privaseeai/ + ls -la /opt/privaseeai/ + ``` + +4. **View system logs:** + ```bash + log show --predicate 'process == "launchd"' --last 5m | grep privaseeai + ``` + +### Service Crashes Immediately + +1. **Check error logs:** + ```bash + tail -50 /var/log/privaseeai/security.log + ``` + +2. **Test manual execution:** + ```bash + cd /opt/privaseeai + python3 -m privaseeai_security.orchestrator + ``` + +3. **Check for missing dependencies:** + ```bash + python3 -c "from privaseeai_security.orchestrator import ThreatOrchestrator" + ``` + +### Logs Not Appearing + +1. **Verify log directory permissions:** + ```bash + ls -la /var/log/privaseeai/ + ``` + +2. **Check if process can write:** + ```bash + sudo -u $(whoami) touch /var/log/privaseeai/test.log + rm /var/log/privaseeai/test.log + ``` + +3. **Ensure PYTHONUNBUFFERED is set** (already in plist) + +## Configuration Reference + +### Key launchd Properties + +- **Label**: `com.privaseeai.security` - Unique service identifier +- **RunAtLoad**: `true` - Start on boot/load +- **KeepAlive**: Configured to restart on any exit +- **ThrottleInterval**: `60` seconds - Prevents rapid restart loops +- **WorkingDirectory**: `/opt/privaseeai` - Where service runs from +- **StandardOutPath/StandardErrorPath**: `/var/log/privaseeai/security.log` - Log location +- **LimitLoadToSessionType**: `Aqua` - User agent (not system daemon) + +### Environment Variables + +The plist includes: +- `PATH`: Standard system PATH +- `PYTHONUNBUFFERED`: `1` - Force unbuffered output for immediate logs + +## Advanced Configuration + +### Custom Python Interpreter + +If you use a virtual environment: + +1. **Edit the plist file:** + ```xml + ProgramArguments + + /path/to/your/venv/bin/python3 + -m + privaseeai_security.orchestrator + + ``` + +2. **Reload the service:** + ```bash + launchctl unload ~/Library/LaunchAgents/com.privaseeai.security.plist + launchctl load ~/Library/LaunchAgents/com.privaseeai.security.plist + ``` + +### Custom Backup Path + +Modify the plist to pass backup path as environment variable: + +```xml +EnvironmentVariables + + + PRIVASEE_BACKUP_PATH + /path/to/ios/backups + +``` + +Then update orchestrator.py to read this environment variable. + +### Adjust Throttle Interval + +Change crash recovery timing: + +```xml +ThrottleInterval +120 +``` + +## Uninstallation + +To completely remove the service: + +```bash +# 1. Unload the service +launchctl unload ~/Library/LaunchAgents/com.privaseeai.security.plist + +# 2. Remove the plist file +rm ~/Library/LaunchAgents/com.privaseeai.security.plist + +# 3. (Optional) Remove log files +sudo rm -rf /var/log/privaseeai/ + +# 4. (Optional) Remove working directory +sudo rm -rf /opt/privaseeai/ +``` + +## Security Considerations + +1. **Never store secrets in the plist file** - Use environment files or macOS Keychain +2. **Limit log file permissions** - Only the service user should read/write logs +3. **Regularly rotate logs** - Use `newsyslog` or similar tools +4. **Monitor for unusual behavior** - Check logs regularly for errors + +## Support + +For issues or questions: +- Check logs: `/var/log/privaseeai/security.log` +- GitHub Issues: https://github.com/aurelianware/PrivaseeAI.Security/issues +- Documentation: See README.md and USER_GUIDE.md + +## Quick Reference Card + +```bash +# Install +cp com.privaseeai.security.plist ~/Library/LaunchAgents/ +launchctl load ~/Library/LaunchAgents/com.privaseeai.security.plist + +# Status +launchctl list | grep com.privaseeai.security + +# Logs +tail -f /var/log/privaseeai/security.log + +# Restart +launchctl stop com.privaseeai.security + +# Uninstall +launchctl unload ~/Library/LaunchAgents/com.privaseeai.security.plist +rm ~/Library/LaunchAgents/com.privaseeai.security.plist +``` diff --git a/com.privaseeai.security.plist b/com.privaseeai.security.plist new file mode 100644 index 0000000..fc8d121 --- /dev/null +++ b/com.privaseeai.security.plist @@ -0,0 +1,64 @@ + + + + + + Label + com.privaseeai.security + + + ProgramArguments + + /usr/bin/python3 + -m + privaseeai_security.orchestrator + + + + WorkingDirectory + /opt/privaseeai + + + RunAtLoad + + + + KeepAlive + + + SuccessfulExit + + + + + + ThrottleInterval + 60 + + + StandardOutPath + /var/log/privaseeai/security.log + + StandardErrorPath + /var/log/privaseeai/security.log + + + EnvironmentVariables + + PATH + /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + PYTHONUNBUFFERED + 1 + + + + Nice + 0 + + + LimitLoadToSessionType + + Aqua + + + diff --git a/src/privaseeai_security/daemon.py b/src/privaseeai_security/daemon.py new file mode 100644 index 0000000..7eb165f --- /dev/null +++ b/src/privaseeai_security/daemon.py @@ -0,0 +1,90 @@ +"""Daemon entry point for PrivaseeAI Security orchestrator. + +This module provides a standalone entry point for running the orchestrator +as a background service via launchd or other init systems. + +Usage: + python -m privaseeai_security.daemon +""" + +import asyncio +import signal +import sys +from pathlib import Path +from typing import Optional + +from .orchestrator import ThreatOrchestrator +from .logger import get_logger + + +logger = get_logger(__name__) + + +# Global orchestrator instance for signal handling +_orchestrator: Optional[ThreatOrchestrator] = None +_shutdown_event = asyncio.Event() + + +def signal_handler(signum, frame): + """Handle shutdown signals gracefully.""" + logger.info(f"Received signal {signum}, initiating shutdown...") + _shutdown_event.set() + + +async def run_daemon(): + """Run the threat orchestrator as a daemon service.""" + global _orchestrator + + logger.info("Starting PrivaseeAI Security daemon...") + + # Create orchestrator instance + _orchestrator = ThreatOrchestrator( + backup_path=None, # Auto-detect + telegram_enabled=True, + monitor_interval=30, + scan_backups_on_start=True + ) + + try: + # Start monitoring + await _orchestrator.start() + logger.info("✅ Orchestrator started successfully") + + # Wait for shutdown signal + await _shutdown_event.wait() + + except Exception as e: + logger.error(f"Daemon error: {e}", exc_info=True) + raise + finally: + # Clean shutdown + if _orchestrator: + logger.info("Stopping orchestrator...") + await _orchestrator.stop() + logger.info("✅ Orchestrator stopped cleanly") + + +def main(): + """Main entry point for daemon.""" + # Setup signal handlers + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + # Log startup + logger.info("PrivaseeAI Security Daemon starting...") + + try: + # Run the async daemon + asyncio.run(run_daemon()) + except KeyboardInterrupt: + logger.info("Daemon interrupted by user") + except Exception as e: + logger.error(f"Fatal error: {e}", exc_info=True) + sys.exit(1) + + logger.info("Daemon shutdown complete") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/src/privaseeai_security/orchestrator.py b/src/privaseeai_security/orchestrator.py index 6a4f600..832d7d8 100644 --- a/src/privaseeai_security/orchestrator.py +++ b/src/privaseeai_security/orchestrator.py @@ -372,3 +372,60 @@ async def scan_now(self) -> ThreatSummary: logger.info("Manual scan triggered") await self._scan_backups_once() return self.get_threat_summary() + + +# Daemon entry point when running as module +async def _run_daemon(): + """Run orchestrator as a daemon service.""" + import signal + + shutdown_event = asyncio.Event() + orchestrator = None + + def signal_handler(signum, frame): + """Handle shutdown signals.""" + logger.info(f"Received signal {signum}, shutting down...") + shutdown_event.set() + + # Setup signal handlers + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + try: + # Create and start orchestrator + orchestrator = ThreatOrchestrator( + backup_path=None, # Auto-detect + telegram_enabled=True, + monitor_interval=30, + scan_backups_on_start=True + ) + + await orchestrator.start() + logger.info("✅ Orchestrator daemon started") + + # Wait for shutdown signal + await shutdown_event.wait() + + except Exception as e: + logger.error(f"Orchestrator daemon error: {e}", exc_info=True) + raise + finally: + if orchestrator: + await orchestrator.stop() + logger.info("✅ Orchestrator daemon stopped") + + +if __name__ == "__main__": + """Entry point for python -m privaseeai_security.orchestrator""" + import sys + + logger.info("Starting PrivaseeAI Security Orchestrator daemon...") + + try: + asyncio.run(_run_daemon()) + except KeyboardInterrupt: + logger.info("Daemon interrupted") + except Exception as e: + logger.error(f"Fatal error: {e}", exc_info=True) + sys.exit(1) + From 3ca3a78dbb6b4169eaacf0b600804bc6218fe7ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:31:48 +0000 Subject: [PATCH 3/6] Add comprehensive tests for launchd service configuration Co-authored-by: aurelianware <194855645+aurelianware@users.noreply.github.com> --- tests/unit/test_launchd_service.py | 137 +++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 tests/unit/test_launchd_service.py diff --git a/tests/unit/test_launchd_service.py b/tests/unit/test_launchd_service.py new file mode 100644 index 0000000..20158bb --- /dev/null +++ b/tests/unit/test_launchd_service.py @@ -0,0 +1,137 @@ +"""Test launchd service and daemon functionality.""" + +import subprocess +import sys +from pathlib import Path +import pytest + + +# Get repository root +REPO_ROOT = Path(__file__).parent.parent.parent + + +def test_plist_file_exists(): + """Test that the plist file exists.""" + plist_path = REPO_ROOT / "com.privaseeai.security.plist" + assert plist_path.exists(), "com.privaseeai.security.plist not found" + + +def test_plist_is_valid_xml(): + """Test that the plist file is valid XML.""" + import xml.etree.ElementTree as ET + + plist_path = REPO_ROOT / "com.privaseeai.security.plist" + tree = ET.parse(plist_path) + root = tree.getroot() + + assert root.tag == "plist", "Root element should be plist" + assert root.attrib.get("version") == "1.0", "plist version should be 1.0" + + +def test_plist_has_required_keys(): + """Test that the plist has all required launchd keys.""" + import xml.etree.ElementTree as ET + + plist_path = REPO_ROOT / "com.privaseeai.security.plist" + tree = ET.parse(plist_path) + + # Check for required keys + required_keys = [ + "Label", + "ProgramArguments", + "RunAtLoad", + "KeepAlive", + "ThrottleInterval", + "StandardOutPath", + "StandardErrorPath", + "WorkingDirectory", + ] + + plist_text = ET.tostring(tree.getroot(), encoding='unicode') + + for key in required_keys: + assert f"{key}" in plist_text, f"Missing required key: {key}" + + +def test_plist_program_arguments(): + """Test that ProgramArguments is correctly configured.""" + import xml.etree.ElementTree as ET + + plist_path = REPO_ROOT / "com.privaseeai.security.plist" + tree = ET.parse(plist_path) + + plist_text = ET.tostring(tree.getroot(), encoding='unicode') + + # Should use python3 -m privaseeai_security.orchestrator + assert "python3" in plist_text, "Should use python3" + assert "privaseeai_security.orchestrator" in plist_text, "Should run orchestrator module" + + +def test_orchestrator_has_main_block(): + """Test that orchestrator.py has __main__ block for daemon mode.""" + from privaseeai_security import orchestrator + + # Check that the module has the main block + import inspect + source = inspect.getsource(orchestrator) + + assert 'if __name__ == "__main__"' in source, "orchestrator.py should have __main__ block" + assert '_run_daemon' in source, "orchestrator.py should have _run_daemon function" + + +def test_orchestrator_can_be_imported(): + """Test that the orchestrator module can be imported.""" + from privaseeai_security.orchestrator import ThreatOrchestrator + + assert ThreatOrchestrator is not None, "Should be able to import ThreatOrchestrator" + + +def test_daemon_module_exists(): + """Test that daemon.py module exists as alternative.""" + from privaseeai_security import daemon + + assert daemon is not None, "daemon module should exist" + assert hasattr(daemon, 'main'), "daemon should have main function" + + +def test_launchd_guide_exists(): + """Test that the LAUNCHD_SERVICE_GUIDE.md exists.""" + guide_path = REPO_ROOT / "LAUNCHD_SERVICE_GUIDE.md" + assert guide_path.exists(), "LAUNCHD_SERVICE_GUIDE.md not found" + + # Check it has key sections + content = guide_path.read_text() + assert "Installation Steps" in content + assert "Loading and Managing the Service" in content + assert "Testing the Service" in content + assert "Troubleshooting" in content + + +@pytest.mark.slow +def test_orchestrator_module_can_run(): + """Test that orchestrator can be run as a module (with quick timeout).""" + # This test tries to run the module but times out quickly + # Just verifying it starts without import errors + import signal + + proc = subprocess.Popen( + [sys.executable, "-m", "privaseeai_security.orchestrator"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + try: + # Let it run for 2 seconds + stdout, stderr = proc.communicate(timeout=2) + # If we get here, process exited early (might be an error) + assert False, f"Process exited unexpectedly: {stderr}" + except subprocess.TimeoutExpired: + # This is expected - the daemon should keep running + proc.kill() + stdout, stderr = proc.communicate() + + # Check for import errors or other startup failures in output + combined = (stdout + stderr).lower() + assert "traceback" not in combined or "modulenotfounderror" not in combined, \ + f"Module had errors: {stdout}\n{stderr}" From 3892e47fdcc03962f393e3f21078e3f13cb8836f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:34:02 +0000 Subject: [PATCH 4/6] Fix code review issues: assertion logic, asyncio event creation, and docstring placement Co-authored-by: aurelianware <194855645+aurelianware@users.noreply.github.com> --- LAUNCHD_QUICK_START.md | 75 +++++++++++++++++++++++++ src/privaseeai_security/daemon.py | 23 +++++--- src/privaseeai_security/orchestrator.py | 2 +- tests/unit/test_launchd_service.py | 2 +- 4 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 LAUNCHD_QUICK_START.md diff --git a/LAUNCHD_QUICK_START.md b/LAUNCHD_QUICK_START.md new file mode 100644 index 0000000..6f1ec86 --- /dev/null +++ b/LAUNCHD_QUICK_START.md @@ -0,0 +1,75 @@ +# Quick Start: launchd Service + +This is a quick reference for installing and running PrivaseeAI Security as a macOS background service. + +## Files Created + +1. **com.privaseeai.security.plist** - launchd configuration file +2. **LAUNCHD_SERVICE_GUIDE.md** - Complete installation and management guide +3. **src/privaseeai_security/daemon.py** - Alternative daemon entry point +4. **Orchestrator daemon mode** - Added `__main__` block to orchestrator.py + +## Quick Installation + +```bash +# 1. Install the package +pip install -e . + +# 2. Create required directories +sudo mkdir -p /var/log/privaseeai /opt/privaseeai +sudo chown $(whoami):staff /var/log/privaseeai /opt/privaseeai + +# 3. Install the service +cp com.privaseeai.security.plist ~/Library/LaunchAgents/ + +# 4. Load and start +launchctl load ~/Library/LaunchAgents/com.privaseeai.security.plist + +# 5. Verify it's running +launchctl list | grep com.privaseeai.security +tail -f /var/log/privaseeai/security.log +``` + +## Service Features + +✅ **Auto-start on boot** - Runs automatically when you log in +✅ **Auto-restart on crash** - Keeps running with 60-second throttle +✅ **Centralized logging** - All output goes to `/var/log/privaseeai/security.log` +✅ **User agent** - Runs as your user, not as root +✅ **Python module execution** - Runs via `python -m privaseeai_security.orchestrator` + +## Quick Commands + +```bash +# Status +launchctl list | grep com.privaseeai.security + +# Logs +tail -f /var/log/privaseeai/security.log + +# Restart +launchctl stop com.privaseeai.security + +# Unload +launchctl unload ~/Library/LaunchAgents/com.privaseeai.security.plist +``` + +## Test the Service + +```bash +# Run the complete test suite +python3 -m pytest tests/unit/test_launchd_service.py -v + +# Test running orchestrator manually +python3 -m privaseeai_security.orchestrator +# Press Ctrl+C to stop +``` + +## For Full Documentation + +See **LAUNCHD_SERVICE_GUIDE.md** for: +- Detailed installation steps +- Configuration options +- Troubleshooting guide +- Advanced customization +- Security considerations diff --git a/src/privaseeai_security/daemon.py b/src/privaseeai_security/daemon.py index 7eb165f..8269f8e 100644 --- a/src/privaseeai_security/daemon.py +++ b/src/privaseeai_security/daemon.py @@ -22,19 +22,32 @@ # Global orchestrator instance for signal handling _orchestrator: Optional[ThreatOrchestrator] = None -_shutdown_event = asyncio.Event() def signal_handler(signum, frame): """Handle shutdown signals gracefully.""" logger.info(f"Received signal {signum}, initiating shutdown...") - _shutdown_event.set() + # Set the shutdown event if it exists in the current context + # The event is created in run_daemon() to avoid asyncio issues async def run_daemon(): """Run the threat orchestrator as a daemon service.""" global _orchestrator + # Create shutdown event inside async context + shutdown_event = asyncio.Event() + + def _signal_handler(signum, frame): + """Handle shutdown signals.""" + logger.info(f"Received signal {signum}, initiating shutdown...") + shutdown_event.set() + + # Setup signal handlers + import signal + signal.signal(signal.SIGTERM, _signal_handler) + signal.signal(signal.SIGINT, _signal_handler) + logger.info("Starting PrivaseeAI Security daemon...") # Create orchestrator instance @@ -51,7 +64,7 @@ async def run_daemon(): logger.info("✅ Orchestrator started successfully") # Wait for shutdown signal - await _shutdown_event.wait() + await shutdown_event.wait() except Exception as e: logger.error(f"Daemon error: {e}", exc_info=True) @@ -66,10 +79,6 @@ async def run_daemon(): def main(): """Main entry point for daemon.""" - # Setup signal handlers - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGINT, signal_handler) - # Log startup logger.info("PrivaseeAI Security Daemon starting...") diff --git a/src/privaseeai_security/orchestrator.py b/src/privaseeai_security/orchestrator.py index 832d7d8..86d1e8c 100644 --- a/src/privaseeai_security/orchestrator.py +++ b/src/privaseeai_security/orchestrator.py @@ -415,8 +415,8 @@ def signal_handler(signum, frame): logger.info("✅ Orchestrator daemon stopped") +# Entry point for python -m privaseeai_security.orchestrator if __name__ == "__main__": - """Entry point for python -m privaseeai_security.orchestrator""" import sys logger.info("Starting PrivaseeAI Security Orchestrator daemon...") diff --git a/tests/unit/test_launchd_service.py b/tests/unit/test_launchd_service.py index 20158bb..0e8157b 100644 --- a/tests/unit/test_launchd_service.py +++ b/tests/unit/test_launchd_service.py @@ -133,5 +133,5 @@ def test_orchestrator_module_can_run(): # Check for import errors or other startup failures in output combined = (stdout + stderr).lower() - assert "traceback" not in combined or "modulenotfounderror" not in combined, \ + assert "traceback" not in combined and "modulenotfounderror" not in combined, \ f"Module had errors: {stdout}\n{stderr}" From d928d53084cc582cca2af24fdec33464ed97bf75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:43:04 +0000 Subject: [PATCH 5/6] Fix coverage configuration to use relative file paths Co-authored-by: aurelianware <194855645+aurelianware@users.noreply.github.com> --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 8fae5c9..63039b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ markers = [ [tool.coverage.run] source = ["src"] +relative_files = true omit = [ "*/tests/*", "*/__pycache__/*", From 096f1a58fb93a2652217f60ec8a6721decae7088 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:52:57 +0000 Subject: [PATCH 6/6] Apply code review feedback: fix Python path, comments, imports, and security Co-authored-by: aurelianware <194855645+aurelianware@users.noreply.github.com> --- LAUNCHD_QUICK_START.md | 3 ++- LAUNCHD_SERVICE_GUIDE.md | 10 +++++----- com.privaseeai.security.plist | 9 +++++---- src/privaseeai_security/daemon.py | 9 --------- tests/unit/test_launchd_service.py | 3 +-- 5 files changed, 13 insertions(+), 21 deletions(-) diff --git a/LAUNCHD_QUICK_START.md b/LAUNCHD_QUICK_START.md index 6f1ec86..0269f89 100644 --- a/LAUNCHD_QUICK_START.md +++ b/LAUNCHD_QUICK_START.md @@ -18,6 +18,7 @@ pip install -e . # 2. Create required directories sudo mkdir -p /var/log/privaseeai /opt/privaseeai sudo chown $(whoami):staff /var/log/privaseeai /opt/privaseeai +chmod 700 /var/log/privaseeai # 3. Install the service cp com.privaseeai.security.plist ~/Library/LaunchAgents/ @@ -33,7 +34,7 @@ tail -f /var/log/privaseeai/security.log ## Service Features ✅ **Auto-start on boot** - Runs automatically when you log in -✅ **Auto-restart on crash** - Keeps running with 60-second throttle +✅ **Auto-restart on crash** - Keeps running with 60-second minimum restart delay ✅ **Centralized logging** - All output goes to `/var/log/privaseeai/security.log` ✅ **User agent** - Runs as your user, not as root ✅ **Python module execution** - Runs via `python -m privaseeai_security.orchestrator` diff --git a/LAUNCHD_SERVICE_GUIDE.md b/LAUNCHD_SERVICE_GUIDE.md index 23ef300..b1a08e5 100644 --- a/LAUNCHD_SERVICE_GUIDE.md +++ b/LAUNCHD_SERVICE_GUIDE.md @@ -8,15 +8,15 @@ The `com.privaseeai.security.plist` file configures PrivaseeAI Security to run a - ✅ Runs `python -m privaseeai_security.orchestrator` automatically - ✅ Starts on boot (user agent) -- ✅ Auto-restarts if crashed (with exponential backoff via ThrottleInterval) +- ✅ Auto-restarts if crashed (with a minimum restart delay enforced by ThrottleInterval) - ✅ Logs to `/var/log/privaseeai/security.log` - ✅ Runs from `/opt/privaseeai` working directory - ✅ Executes as the current user -- ✅ Prevents rapid restart loops with 60-second throttle +- ✅ Prevents rapid restart loops with a fixed 60-second ThrottleInterval between restarts ## Prerequisites -1. **Python 3.11+** installed at `/usr/bin/python3` +1. **Python 3.11+** - Ensure a Python 3.11+ interpreter is available in your PATH (via Homebrew, venv, or system Python) 2. **PrivaseeAI Security** installed as a Python package 3. **Proper permissions** to write to `/var/log/privaseeai/` @@ -45,8 +45,8 @@ sudo mkdir -p /var/log/privaseeai # Set ownership to current user sudo chown $(whoami):staff /var/log/privaseeai -# Set appropriate permissions -chmod 755 /var/log/privaseeai +# Restrict permissions so only this user can access logs +chmod 700 /var/log/privaseeai ``` ### 3. Set Up Working Directory diff --git a/com.privaseeai.security.plist b/com.privaseeai.security.plist index fc8d121..aea6044 100644 --- a/com.privaseeai.security.plist +++ b/com.privaseeai.security.plist @@ -9,7 +9,8 @@ ProgramArguments - /usr/bin/python3 + /usr/bin/env + python3 -m privaseeai_security.orchestrator @@ -25,13 +26,13 @@ KeepAlive - + SuccessfulExit - - + + ThrottleInterval 60 diff --git a/src/privaseeai_security/daemon.py b/src/privaseeai_security/daemon.py index 8269f8e..a240bdf 100644 --- a/src/privaseeai_security/daemon.py +++ b/src/privaseeai_security/daemon.py @@ -10,7 +10,6 @@ import asyncio import signal import sys -from pathlib import Path from typing import Optional from .orchestrator import ThreatOrchestrator @@ -24,13 +23,6 @@ _orchestrator: Optional[ThreatOrchestrator] = None -def signal_handler(signum, frame): - """Handle shutdown signals gracefully.""" - logger.info(f"Received signal {signum}, initiating shutdown...") - # Set the shutdown event if it exists in the current context - # The event is created in run_daemon() to avoid asyncio issues - - async def run_daemon(): """Run the threat orchestrator as a daemon service.""" global _orchestrator @@ -44,7 +36,6 @@ def _signal_handler(signum, frame): shutdown_event.set() # Setup signal handlers - import signal signal.signal(signal.SIGTERM, _signal_handler) signal.signal(signal.SIGINT, _signal_handler) diff --git a/tests/unit/test_launchd_service.py b/tests/unit/test_launchd_service.py index 0e8157b..adfb6ee 100644 --- a/tests/unit/test_launchd_service.py +++ b/tests/unit/test_launchd_service.py @@ -112,7 +112,6 @@ def test_orchestrator_module_can_run(): """Test that orchestrator can be run as a module (with quick timeout).""" # This test tries to run the module but times out quickly # Just verifying it starts without import errors - import signal proc = subprocess.Popen( [sys.executable, "-m", "privaseeai_security.orchestrator"], @@ -125,7 +124,7 @@ def test_orchestrator_module_can_run(): # Let it run for 2 seconds stdout, stderr = proc.communicate(timeout=2) # If we get here, process exited early (might be an error) - assert False, f"Process exited unexpectedly: {stderr}" + pytest.fail(f"Process exited unexpectedly: {stderr}") except subprocess.TimeoutExpired: # This is expected - the daemon should keep running proc.kill()