Build a Secure AI Linux Assistant in Python: Monitor, Update, Troubleshoot, and Notify (API + Hardening)Build a secure AI assistant for Linux in Python that monitors CPU/RAM/disk, summarizes logs, sends notifications, and performs tightly controlled updates and service restarts. Follow a step-by-step plan-then-execute design with allowlists, sudo least privilege, and systemd hardening to prevent unsafe command execution.

Table of Contents

Introduction: What you’ll build or accomplish

You will build a practical AI assistant for a Linux system that can:

  • Monitor CPU, RAM, disk, network, and key services
  • Inform you via terminal output and desktop notifications
  • Update the system safely (opt-in, least privilege)
  • Troubleshoot common issues by summarizing logs and proposing fixes
  • Use an AI API (OpenAI-compatible) to explain problems and recommend actions
  • Harden security so AI output cannot directly run arbitrary commands

You will implement a “plan-then-execute” workflow:

  1. Collect system facts (metrics + logs)
  2. Ask the AI to produce a structured JSON plan
  3. Validate the plan against a strict allowlist
  4. Execute only safe, allowed actions
  5. Log everything and notify the user

This pattern is necessary because letting an LLM generate shell commands directly is risky. Your assistant will treat the LLM as an advisor, not an unrestricted root shell.

Prerequisites: Required tools, knowledge, or setup

System requirements

  • A Linux machine (Ubuntu/Debian examples used; notes included for others)
  • Python 3.9+ (3.11/3.12 recommended)
  • Basic terminal familiarity (editing files, running commands)

Packages you’ll install

  • psutil for system metrics
  • python-dotenv for secure environment loading
  • requests for API calls (or OpenAI SDK if you prefer)
  • systemd-python optional (we’ll use systemd via unit files)

Security assumptions

  • You will not hardcode API keys
  • You will run the assistant as a non-root user by default
  • You will elevate privileges only for narrowly-scoped update actions

Step 1: Create a project folder and Python virtual environment

Do this to isolate dependencies and prevent contaminating system Python packages.

Commands

mkdir -p ~/linux-ai-assistant
cd ~/linux-ai-assistant

python3 -m venv .venv
source .venv/bin/activate

python -V

Expected results

  • python -V prints something like Python 3.11.x
  • Your shell prompt often shows (.venv)

Common errors

  • “python3: command not found”: install Python (e.g., sudo apt install -y python3 python3-venv).
  • Permission issues: ensure you are in your home directory and not a protected path.

Step 2: Install dependencies

Do this so you can read system metrics, send notifications, and call the AI API.

Commands

pip install --upgrade pip
pip install psutil python-dotenv requests

Optional (desktop notifications)

If you are on a desktop environment, install notify-send (usually already available):

sudo apt update
sudo apt install -y libnotify-bin

Expected results

  • pip completes without errors
  • python -c "import psutil" exits silently (success)

Step 3: Create a secure configuration (.env) and ignore it

Do this to keep secrets out of your source code and out of Git history.

Create .env

cat > .env <<'EOF'
# AI API configuration
AI_API_BASE_URL=https://api.openai.com/v1
AI_MODEL=gpt-4o-mini
OPENAI_API_KEY=REPLACE_ME

# Assistant behavior
ASSISTANT_LOG_LEVEL=INFO
EOF

Add .env to .gitignore

cat > .gitignore <<'EOF'
.venv/
__pycache__/
*.pyc
.env
logs/
EOF

Expected results

  • Your API key is stored only in .env
  • If you later use Git, .env won’t be committed

Warning (security)

Never paste API keys into screenshots, issue trackers, or public repos. Rotate keys if you accidentally expose them.

Step 4: Create the project structure

Do this to keep the assistant maintainable and secure. Separation of concerns makes it harder to accidentally execute unsafe actions.

Commands

mkdir -p assistant logs

touch assistant/__init__.py \
      assistant/config.py \
      assistant/ai_client.py \
      assistant/monitor.py \
      assistant/notify.py \
      assistant/security.py \
      assistant/actions.py \
      assistant/main.py

Expected results

  • You have a clean module layout under assistant/
  • Logs will be stored in logs/

Step 5: Implement configuration loading and logging

Do this so the assistant reads settings from environment variables and produces auditable logs (critical for troubleshooting and security review).

assistant/config.py

import os
from dataclasses import dataclass
from dotenv import load_dotenv

load_dotenv()

@dataclass(frozen=True)
class Settings:
    api_base_url: str
    api_key: str
    model: str
    log_level: str


def get_settings() -> Settings:
    api_base_url = os.getenv("AI_API_BASE_URL", "https://api.openai.com/v1")
    api_key = os.getenv("OPENAI_API_KEY", "")
    model = os.getenv("AI_MODEL", "gpt-4o-mini")
    log_level = os.getenv("ASSISTANT_LOG_LEVEL", "INFO")

    if not api_key or api_key == "REPLACE_ME":
        raise RuntimeError("OPENAI_API_KEY is not set. Put it in .env")

    return Settings(
        api_base_url=api_base_url,
        api_key=api_key,
        model=model,
        log_level=log_level,
    )

Expected results

  • If OPENAI_API_KEY is missing, the app fails fast with a clear error

Step 6: Implement the AI API client (safe, bounded output)

Do this so the assistant can ask the model for explanations and structured plans. You will force the response into a predictable JSON schema to reduce prompt injection and unexpected output.

assistant/ai_client.py

import json
import requests
from typing import Any, Dict

from .config import Settings


class AIClient:
    def __init__(self, settings: Settings):
        self.settings = settings

    def propose_plan(self, context: Dict[str, Any], user_request: str) -> Dict[str, Any]:
        """Ask the LLM to return a strict JSON plan.

        The plan is advisory. Execution will be validated against an allowlist.
        """

        system_prompt = (
            "You are a Linux operations assistant. "
            "Return ONLY valid JSON (no markdown, no prose). "
            "Schema: {\n"
            "  \"summary\": string,\n"
            "  \"risk\": \"low\"|\"medium\"|\"high\",\n"
            "  \"actions\": [\n"
            "    {\"type\": \"notify\"|\"read_only\"|\"update\"|\"service\",\n"
            "     \"name\": string,\n"
            "     \"args\": object}\n"
            "  ]\n"
            "} "
            "Constraints: do not request arbitrary shell, do not include secrets, "
            "prefer read_only actions, and set risk=high if any update/restart is suggested."
        )

        payload = {
            "model": self.settings.model,
            "messages": [
                {"role": "system", "content": system_prompt},
                {
                    "role": "user",
                    "content": json.dumps(
                        {"user_request": user_request, "context": context},
                        ensure_ascii=False,
                    ),
                },
            ],
            "temperature": 0.2,
        }

        url = f"{self.settings.api_base_url}/chat/completions"
        headers = {
            "Authorization": f"Bearer {self.settings.api_key}",
            "Content-Type": "application/json",
        }

        resp = requests.post(url, headers=headers, json=payload, timeout=60)
        resp.raise_for_status()
        data = resp.json()

        content = data["choices"][0]["message"]["content"].strip()
        return json.loads(content)

Expected results

  • The model returns JSON you can parse
  • If the model returns non-JSON, json.loads fails (you will handle this later)

Common errors

  • 401 Unauthorized: API key missing/invalid.
  • 404: wrong AI_API_BASE_URL.
  • JSON decode error: model returned text. You will add retry/repair in troubleshooting.

Step 7: Implement system monitoring (CPU/RAM/disk/network + top processes)

Do this to provide the AI with real context. Without metrics, the AI guesses. With metrics, it can explain spikes and recommend targeted actions.

assistant/monitor.py

import time
import psutil
from typing import Any, Dict, List


def get_metrics() -> Dict[str, Any]:
    cpu = psutil.cpu_percent(interval=0.5)
    vm = psutil.virtual_memory()
    du = psutil.disk_usage("/")

    net1 = psutil.net_io_counters()
    time.sleep(0.3)
    net2 = psutil.net_io_counters()

    return {
        "cpu_percent": cpu,
        "ram_percent": vm.percent,
        "ram_used_gb": round(vm.used / (1024**3), 2),
        "ram_total_gb": round(vm.total / (1024**3), 2),
        "disk_percent_root": du.percent,
        "disk_used_gb": round(du.used / (1024**3), 2),
        "disk_total_gb": round(du.total / (1024**3), 2),
        "net_bytes_sent_per_s": int((net2.bytes_sent - net1.bytes_sent) / 0.3),
        "net_bytes_recv_per_s": int((net2.bytes_recv - net1.bytes_recv) / 0.3),
        "boot_time": psutil.boot_time(),
    }


def get_top_processes(limit: int = 5) -> List[Dict[str, Any]]:
    procs = []
    for p in psutil.process_iter(attrs=["pid", "name", "username", "cpu_percent", "memory_percent"]):
        try:
            procs.append(p.info)
        except psutil.Error:
            continue

    procs.sort(key=lambda x: (x.get("cpu_percent") or 0, x.get("memory_percent") or 0), reverse=True)
    return procs[:limit]

Expected results

  • Metrics show current system usage
  • Top processes list identifies likely culprits for CPU/RAM spikes

Step 8: Implement notifications (terminal + desktop)

Do this so the assistant can inform you without you constantly watching logs. Notifications are also useful when running as a background service.

assistant/notify.py

import subprocess
from typing import Optional


def notify(title: str, message: str, urgency: str = "normal") -> None:
    """Send a desktop notification if available; otherwise do nothing."""
    try:
        subprocess.run(
            ["notify-send", "-u", urgency, title, message],
            check=False,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
    except FileNotFoundError:
        # notify-send not installed or no desktop environment
        pass


def print_banner(text: str) -> None:
    print("\n" + "=" * 80)
    print(text)
    print("=" * 80)

Expected results

  • On desktops, you see a system popup
  • On servers, it quietly degrades without failing

Screenshot description

Expect a notification popup in the top-right (GNOME) showing: title “AI Assistant” and the message body (e.g., “Disk usage is 92% on /”).

Step 9: Add a strict security layer (allowlist + input validation)

Do this to prevent command injection and “LLM said so” dangerous execution. The assistant must only run known-safe actions you explicitly allow.

assistant/security.py

from dataclasses import dataclass
from typing import Any, Dict, List


ALLOWED_ACTION_TYPES = {"notify", "read_only", "update", "service"}

# Whitelist action names
ALLOWED_READ_ONLY = {
    "disk_report",
    "memory_report",
    "cpu_report",
    "journal_errors",
    "service_status",
}

ALLOWED_UPDATE = {
    "apt_update_upgrade",
}

ALLOWED_SERVICE = {
    "restart_service",
}

# Restrict which services can be manipulated
ALLOWED_SERVICE_NAMES = {
    "ssh",
    "cron",
    "network-manager",
    "docker",
}


@dataclass
class ValidationResult:
    ok: bool
    errors: List[str]


def validate_plan(plan: Dict[str, Any]) -> ValidationResult:
    errors: List[str] = []

    if not isinstance(plan, dict):
        return ValidationResult(False, ["Plan is not an object"]) 

    if "actions" not in plan or not isinstance(plan["actions"], list):
        return ValidationResult(False, ["Plan.actions must be a list"]) 

    for i, action in enumerate(plan["actions"]):
        if not isinstance(action, dict):
            errors.append(f"Action {i} is not an object")
            continue

        a_type = action.get("type")
        name = action.get("name")
        args = action.get("args", {})

        if a_type not in ALLOWED_ACTION_TYPES:
            errors.append(f"Action {i}: type not allowed: {a_type}")
            continue

        if not isinstance(name, str) or not name:
            errors.append(f"Action {i}: name must be a non-empty string")
            continue

        if not isinstance(args, dict):
            errors.append(f"Action {i}: args must be an object")
            continue

        if a_type == "read_only" and name not in ALLOWED_READ_ONLY:
            errors.append(f"Action {i}: read_only name not allowed: {name}")

        if a_type == "update" and name not in ALLOWED_UPDATE:
            errors.append(f"Action {i}: update name not allowed: {name}")

        if a_type == "service":
            if name not in ALLOWED_SERVICE:
                errors.append(f"Action {i}: service name not allowed: {name}")
            svc = args.get("service")
            if svc not in ALLOWED_SERVICE_NAMES:
                errors.append(
                    f"Action {i}: service '{svc}' not in allowed list: {sorted(ALLOWED_SERVICE_NAMES)}"
                )

    return ValidationResult(ok=(len(errors) == 0), errors=errors)

Expected results

  • Any plan outside your safe set is rejected
  • You can expand the allowlist later, deliberately

Warning (security)

Do not add a “shell” action type. The moment you allow arbitrary shell, you lose meaningful control over execution.

Step 10: Implement safe actions (read-only, update, service control)

Do this so execution is deterministic, testable, and auditable. You will run system commands only through controlled code paths.

assistant/actions.py

import subprocess
from typing import Any, Dict


def _run(cmd: list[str], timeout: int = 60) -> Dict[str, Any]:
    p = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        timeout=timeout,
        check=False,
    )
    return {
        "cmd": cmd,
        "returncode": p.returncode,
        "stdout": p.stdout[-4000:],
        "stderr": p.stderr[-4000:],
    }


def disk_report() -> Dict[str, Any]:
    return _run(["df", "-h"])


def memory_report() -> Dict[str, Any]:
    return _run(["free", "-h"])


def cpu_report() -> Dict[str, Any]:
    return _run(["uptime"])  # simple load average


def journal_errors(lines: int = 200) -> Dict[str, Any]:
    return _run(["journalctl", "-p", "3", "-n", str(lines), "--no-pager"], timeout=90)


def service_status(service: str) -> Dict[str, Any]:
    return _run(["systemctl", "status", service, "--no-pager"], timeout=30)


def apt_update_upgrade() -> Dict[str, Any]:
    """Perform apt update/upgrade.

    WARNING: This changes system state. Require explicit user consent in main flow.
    """
    # Use non-interactive behavior
    env = {"DEBIAN_FRONTEND": "noninteractive"}

    r1 = subprocess.run(["sudo", "-n", "apt-get", "update"], capture_output=True, text=True)
    r2 = subprocess.run(
        ["sudo", "-n", "apt-get", "upgrade", "-y"],
        capture_output=True,
        text=True,
        env=env,
    )
    return {
        "cmd": ["sudo", "-n", "apt-get", "update", "&&", "sudo", "-n", "apt-get", "upgrade", "-y"],
        "returncode": (r1.returncode or r2.returncode),
        "stdout": (r1.stdout + "\n" + r2.stdout)[-4000:],
        "stderr": (r1.stderr + "\n" + r2.stderr)[-4000:],
    }


def restart_service(service: str) -> Dict[str, Any]:
    """Restart an allowed service.

    WARNING: This changes system state and can disrupt connectivity.
    """
    return _run(["sudo", "-n", "systemctl", "restart", service], timeout=60)

Expected results

  • Read-only actions return command output and errors for the AI to summarize
  • Update/service actions require sudo and can fail safely if not permitted

Warnings (destructive actions)

  • Updates can break packages or require reboots.
  • Service restarts can drop SSH or networking if you restart the wrong thing.

Step 11: Build the main CLI assistant (plan → validate → execute)

Do this to connect monitoring, AI planning, validation, and action execution into a single tool you can run manually or via systemd.

assistant/main.py

import argparse
import json
import logging
import os
from datetime import datetime
from typing import Any, Dict

from .actions import (
    apt_update_upgrade,
    cpu_report,
    disk_report,
    journal_errors,
    memory_report,
    restart_service,
    service_status,
)
from .ai_client import AIClient
from .config import get_settings
from .monitor import get_metrics, get_top_processes
from .notify import notify, print_banner
from .security import validate_plan


ACTION_DISPATCH = {
    "disk_report": lambda args: disk_report(),
    "memory_report": lambda args: memory_report(),
    "cpu_report": lambda args: cpu_report(),
    "journal_errors": lambda args: journal_errors(lines=int(args.get("lines", 120))),
    "service_status": lambda args: service_status(service=str(args["service"])),
    "apt_update_upgrade": lambda args: apt_update_upgrade(),
    "restart_service": lambda args: restart_service(service=str(args["service"])),
}


def build_context() -> Dict[str, Any]:
    return {
        "metrics": get_metrics(),
        "top_processes": get_top_processes(5),
        "time": datetime.utcnow().isoformat() + "Z",
        "hostname": os.uname().nodename,
    }


def execute_plan(plan: Dict[str, Any], assume_yes: bool = False) -> Dict[str, Any]:
    results = {"summary": plan.get("summary"), "risk": plan.get("risk"), "actions": []}

    for action in plan["actions"]:
        a_type = action["type"]
        name = action["name"]
        args = action.get("args", {})

        # Require explicit consent for state-changing actions
        if a_type in {"update", "service"} and not assume_yes:
            print_banner("CONFIRMATION REQUIRED")
            print(json.dumps(action, indent=2))
            answer = input("Proceed? (yes/no): ").strip().lower()
            if answer != "yes":
                results["actions"].append({"action": action, "skipped": True, "reason": "user declined"})
                continue

        if a_type == "notify":
            title = str(args.get("title", "AI Assistant"))
            msg = str(args.get("message", plan.get("summary", "Notification")))
            notify(title, msg, urgency=str(args.get("urgency", "normal")))
            results["actions"].append({"action": action, "ok": True, "result": "notified"})
            continue

        # Execute mapped actions
        fn = ACTION_DISPATCH.get(name)
        if not fn:
            results["actions"].append({"action": action, "ok": False, "error": "no dispatcher"})
            continue

        out = fn(args)
        results["actions"].append({"action": action, "ok": (out.get("returncode", 1) == 0), "output": out})

    return results


def main() -> None:
    parser = argparse.ArgumentParser(description="Secure AI Linux assistant")
    parser.add_argument("request", nargs="?", default="status", help="What you want the assistant to do")
    parser.add_argument("--yes", action="store_true", help="Skip confirmations (DANGEROUS)")
    args = parser.parse_args()

    settings = get_settings()

    logging.basicConfig(
        level=getattr(logging, settings.log_level.upper(), logging.INFO),
        format="%(asctime)s %(levelname)s %(message)s",
    )

    context = build_context()

    print_banner("LOCAL SYSTEM CONTEXT")
    print(json.dumps(context, indent=2))

    client = AIClient(settings)

    try:
        plan = client.propose_plan(context=context, user_request=args.request)
    except Exception as e:
        print_banner("AI ERROR")
        print(f"Failed to get plan: {e}")
        raise

    print_banner("PROPOSED PLAN (from AI)")
    print(json.dumps(plan, indent=2))

    vr = validate_plan(plan)
    if not vr.ok:
        print_banner("PLAN REJECTED (SECURITY)")
        for err in vr.errors:
            print(f"- {err}")
        print("\nNo actions were executed.")
        return

    result = execute_plan(plan, assume_yes=args.yes)

    print_banner("EXECUTION RESULTS")
    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    main()

Expected results

  • You see a LOCAL SYSTEM CONTEXT JSON block
  • You see a PROPOSED PLAN JSON block
  • If the plan is allowed, you see EXECUTION RESULTS with command output
  • If the plan is unsafe, it is rejected and nothing runs

Warning (dangerous flag)

--yes skips confirmations. Use it only in controlled environments after you trust your allowlist and sudo rules.

Step 12: Run the assistant (manual mode)

Do this to verify end-to-end functionality before daemonizing it.

Commands

source .venv/bin/activate
python -m assistant.main "check disk and show errors"

Expected results

A typical safe plan might include read-only actions:

{
  "summary": "Disk usage is high; gathering disk and journal error information.",
  "risk": "low",
  "actions": [
    {"type": "read_only", "name": "disk_report", "args": {}},
    {"type": "read_only", "name": "journal_errors", "args": {"lines": 120}},
    {"type": "notify", "name": "notify_user", "args": {"title": "AI Assistant", "message": "Collected disk and error logs."}}
  ]
}

Then you should see df -h output and journal excerpts in the execution results.

Step 13: Add sudo rules for narrowly-scoped privilege elevation

Do this so update/restart actions can run without giving the assistant full root access. The goal is least privilege.

Why this is necessary

  • Running the entire assistant as root is dangerous (AI-driven logic + root is a bad combo).
  • Allow only specific commands via sudoers.

Create a sudoers file (Ubuntu/Debian)

Warning: Editing sudoers incorrectly can lock you out of admin access. Use visudo.

sudo visudo -f /etc/sudoers.d/ai-assistant

Paste this, replacing YOURUSER with your Linux username:

# Allow only specific commands without password
YOURUSER ALL=(root) NOPASSWD: /usr/bin/apt-get update
YOURUSER ALL=(root) NOPASSWD: /usr/bin/apt-get upgrade -y
YOURUSER ALL=(root) NOPASSWD: /bin/systemctl restart ssh
YOURUSER ALL=(root) NOPASSWD: /bin/systemctl restart cron
YOURUSER ALL=(root) NOPASSWD: /bin/systemctl restart docker
YOURUSER ALL=(root) NOPASSWD: /bin/systemctl restart network-manager

Expected results

  • sudo -n apt-get update runs without prompting for a password
  • Any other sudo command still requires a password (or is denied)

Common errors

  • “sudo: a password is required”: you did not configure NOPASSWD correctly or command path differs.
  • Wrong binary paths: check with which apt-get and which systemctl.

Step 14: Run the assistant as a systemd service (background monitoring)

Do this to monitor continuously and notify you when thresholds are exceeded or errors spike.

Implement a simple monitor loop command

Add a small file to run periodic checks.

assistant/daemon.py (create it)

cat > assistant/daemon.py <<'EOF'
import time
import json
from assistant.config import get_settings
from assistant.ai_client import AIClient
from assistant.monitor import get_metrics, get_top_processes
from assistant.notify import notify
from assistant.security import validate_plan
from assistant.main import execute_plan


def build_context():
    return {"metrics": get_metrics(), "top_processes": get_top_processes(5)}


def run_forever(interval_s: int = 60):
    settings = get_settings()
    client = AIClient(settings)

    while True:
        context = build_context()

        # You control the monitoring request here.
        request = (
            "Monitor system health. If disk > 90% or RAM > 90% or CPU > 95% then notify. "
            "If journal has many priority-3 errors, summarize and notify. "
            "Use read_only actions only."
        )

        try:
            plan = client.propose_plan(context=context, user_request=request)
        except Exception:
            time.sleep(interval_s)
            continue

        vr = validate_plan(plan)
        if vr.ok:
            # Never auto-run updates in daemon mode; keep it read-only.
            res = execute_plan(plan, assume_yes=False)
            # Optional: if anything failed, send a notification
            if any(a.get("ok") is False for a in res.get("actions", [])):
                notify("AI Assistant", "One or more monitoring actions failed.")

        time.sleep(interval_s)


if __name__ == "__main__":
    run_forever(60)
EOF

Create a systemd unit

sudo tee /etc/systemd/system/ai-assistant.service > /dev/null <<'EOF'
[Unit]
Description=AI Linux Assistant (secure monitor)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=%i
WorkingDirectory=/home/%i/linux-ai-assistant
Environment=PYTHONUNBUFFERED=1
ExecStart=/home/%i/linux-ai-assistant/.venv/bin/python -m assistant.daemon
Restart=on-failure
RestartSec=5

# Security hardening (systemd sandboxing)
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/home/%i/linux-ai-assistant/logs
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true

[Install]
WantedBy=multi-user.target
EOF

Important: this uses an instance unit with %i. Enable it like this (replace YOURUSER):

sudo systemctl daemon-reload
sudo systemctl enable --now [email protected]

sudo systemctl status [email protected] --no-pager

Expected results

  • status shows the service as active (running)
  • You receive notifications when thresholds are exceeded (desktop) or see logs in journald

Screenshot description

In systemctl status, expect a green “active (running)” line and recent log lines showing the assistant loop iterations.

Troubleshooting: Common issues and solutions

1) The AI returns non-JSON and the assistant crashes

Why it happens: models sometimes ignore formatting constraints.

Fix: add a retry with a “repair” prompt, or reject and run read-only defaults.

Action you can take now: lower temperature (already low) and keep the system prompt strict. If it persists, wrap json.loads with try/except and re-ask: “Return only JSON.”

2) notify-send does nothing

Why it happens: no desktop session (server), or systemd service has no GUI session DBus.

Fix: rely on journald logs for servers, or send notifications via email/Slack/Telegram instead. For desktop services, you may need to run as a user session service.

3) sudo -n fails

Why it happens: sudoers not configured for passwordless for the exact command path and args.

Fix: verify binary paths with which apt-get/which systemctl and ensure the sudoers lines match exactly.

4) systemd hardening breaks access to files

Why it happens: ProtectHome=true blocks home directory reads.

Fix: loosen to ProtectHome=read-only or adjust ReadWritePaths/WorkingDirectory. If you store .env in the home directory, ensure systemd can read it.

5) Monitoring reports CPU spikes but top processes look wrong

Why it happens: process CPU% requires sampling time; first read can be 0.

Fix: call cpu_percent twice with a small interval if you need accurate per-process CPU.

Testing: How to verify it works

Test 1: Read-only diagnostics

python -m assistant.main "summarize current health and show journal errors"

Expected: a plan containing disk_report and/or journal_errors, plus command outputs in results.

Test 2: Plan rejection (security)

Try to trick it:

python -m assistant.main "run rm -rf / to fix disk"

Expected: the plan is rejected because there is no allowed “shell” action type. No destructive actions run.

Test 3: Updates (controlled)

python -m assistant.main "update my system packages"

Expected: assistant requests confirmation. If sudoers is configured, update can proceed; otherwise it fails safely with a sudo error in stderr.

Test 4: Service control (controlled)

python -m assistant.main "restart docker if it is unhealthy"

Expected: if the AI proposes restart_service with an allowed service, you are prompted. If it proposes a non-allowed service, the plan is rejected.

Next Steps: Ways to extend or improve

  • Add more read-only collectors: SMART disk health (smartctl), GPU stats, RAID status.
  • Add log summarization pipelines: collect journalctl for specific units (e.g., journalctl -u docker) and ask the AI to summarize.
  • Use a local LLM for privacy: swap the API client to call a local model server (e.g., Ollama) so logs never leave the machine.
  • Add RBAC and multi-user support: different policies per user (admins can update, regular users can only read).
  • Add a web API: expose a small FastAPI endpoint that returns metrics and AI plans for your dashboard. Put it behind authentication and TLS.
  • Improve auditing: write all executed actions to an append-only log and ship to a SIEM.

Conclusion: Summary and additional resources

You built a secure, practical AI assistant for Linux in Python that monitors system health, summarizes issues, and can perform controlled updates and service actions. You implemented the key safety pattern: LLM proposes a plan, your code validates it, and only then executes allowlisted operations. You also added least-privilege sudo rules and optional systemd hardening so the assistant is usable in production-like environments.

To keep improving: expand your allowlist slowly, log everything, keep updates opt-in, and consider local inference if you need stronger privacy guarantees.

Leave a Reply