mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Initial commit: JARVIS AI dashboard v2.3
- 4-tier chat: HA control → Ollama → Groq → Claude - Push-based agent system with heartbeat/metrics - Network monitoring, alerts, Proxmox, Home Assistant - Windows + Linux agent installers - Stats cache cron, facts collector, KB engine
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
# JARVIS Agent Installer — macOS
|
||||
# Usage: bash install-mac.sh --jarvis-url https://jarvis.orbishosting.com --key YOUR_KEY
|
||||
|
||||
set -e
|
||||
|
||||
JARVIS_URL=""
|
||||
REG_KEY=""
|
||||
INSTALL_DIR="$HOME/.jarvis-agent"
|
||||
CONFIG_DIR="$HOME/.jarvis-agent"
|
||||
PLIST_PATH="$HOME/Library/LaunchAgents/com.jarvis.agent.plist"
|
||||
SERVICE_LABEL="com.jarvis.agent"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--jarvis-url) JARVIS_URL="$2"; shift 2 ;;
|
||||
--key) REG_KEY="$2"; shift 2 ;;
|
||||
*) echo "Unknown arg: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$JARVIS_URL" ]]; then
|
||||
read -rp "JARVIS URL (e.g. https://jarvis.orbishosting.com): " JARVIS_URL
|
||||
fi
|
||||
if [[ -z "$REG_KEY" ]]; then
|
||||
read -rp "Registration key: " REG_KEY
|
||||
fi
|
||||
|
||||
JARVIS_URL="${JARVIS_URL%/}"
|
||||
|
||||
# Check for Python3
|
||||
PYTHON3=$(command -v python3 2>/dev/null || command -v /usr/bin/python3 2>/dev/null || "")
|
||||
if [[ -z "$PYTHON3" ]]; then
|
||||
echo "Python 3 is required. Install it with:"
|
||||
echo " brew install python3"
|
||||
echo " or download from https://www.python.org/downloads/"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using Python: $PYTHON3 ($($PYTHON3 --version))"
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Download or copy agent script
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/jarvis-agent.py" ]]; then
|
||||
cp "$SCRIPT_DIR/jarvis-agent.py" "$INSTALL_DIR/jarvis-agent.py"
|
||||
else
|
||||
echo "Downloading agent..."
|
||||
curl -sSL "https://raw.githubusercontent.com/myronblair/jarvis/master/agent/jarvis-agent.py" \
|
||||
-o "$INSTALL_DIR/jarvis-agent.py"
|
||||
fi
|
||||
chmod +x "$INSTALL_DIR/jarvis-agent.py"
|
||||
|
||||
HOSTNAME=$(hostname -f 2>/dev/null || hostname)
|
||||
|
||||
# Write config
|
||||
if [[ ! -f "$CONFIG_DIR/config.json" ]]; then
|
||||
cat > "$CONFIG_DIR/config.json" << JSONEOF
|
||||
{
|
||||
"jarvis_url": "$JARVIS_URL",
|
||||
"registration_key": "$REG_KEY",
|
||||
"hostname": "$HOSTNAME",
|
||||
"agent_type": "linux",
|
||||
"poll_interval": 30,
|
||||
"heartbeat_every": 10,
|
||||
"watch_services": []
|
||||
}
|
||||
JSONEOF
|
||||
chmod 600 "$CONFIG_DIR/config.json"
|
||||
fi
|
||||
|
||||
# Override state path in agent for macOS
|
||||
STATE_PATH="$INSTALL_DIR/state.json"
|
||||
|
||||
# Write launchd plist
|
||||
mkdir -p "$HOME/Library/LaunchAgents"
|
||||
cat > "$PLIST_PATH" << PLISTEOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>$SERVICE_LABEL</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$PYTHON3</string>
|
||||
<string>$INSTALL_DIR/jarvis-agent.py</string>
|
||||
</array>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>JARVIS_CONFIG</key>
|
||||
<string>$CONFIG_DIR/config.json</string>
|
||||
<key>JARVIS_STATE</key>
|
||||
<string>$STATE_PATH</string>
|
||||
</dict>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>$INSTALL_DIR/jarvis-agent.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>$INSTALL_DIR/jarvis-agent.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLISTEOF
|
||||
|
||||
# Load the service
|
||||
launchctl unload "$PLIST_PATH" 2>/dev/null || true
|
||||
launchctl load "$PLIST_PATH"
|
||||
|
||||
sleep 2
|
||||
if launchctl list | grep -q "$SERVICE_LABEL"; then
|
||||
echo ""
|
||||
echo "✓ JARVIS Agent installed and running."
|
||||
echo " View logs: tail -f $INSTALL_DIR/jarvis-agent.log"
|
||||
echo " Config: $CONFIG_DIR/config.json"
|
||||
echo " Stop: launchctl unload $PLIST_PATH"
|
||||
else
|
||||
echo "⚠ Agent installed but not running. Check logs:"
|
||||
echo " tail -f $INSTALL_DIR/jarvis-agent.log"
|
||||
fi
|
||||
@@ -0,0 +1,152 @@
|
||||
# JARVIS Agent Installer — Windows (PowerShell)
|
||||
# Run as Administrator:
|
||||
# Set-ExecutionPolicy Bypass -Scope Process
|
||||
# .\install-windows.ps1 -JarvisUrl https://jarvis.orbishosting.com -Key YOUR_KEY
|
||||
# Or one-liner (from PowerShell as Admin):
|
||||
# irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
|
||||
|
||||
param(
|
||||
[string]$JarvisUrl = "https://jarvis.orbishosting.com",
|
||||
[string]$Key = "",
|
||||
[string]$AgentName = $env:COMPUTERNAME.ToLower()
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$InstallDir = "C:\ProgramData\jarvis-agent"
|
||||
$AgentScript = "$InstallDir\jarvis-agent.py"
|
||||
$ConfigFile = "$InstallDir\config.json"
|
||||
$TaskName = "JARVIS-Agent"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " ====================================" -ForegroundColor Cyan
|
||||
Write-Host " JARVIS Agent Installer v2.2 " -ForegroundColor Cyan
|
||||
Write-Host " ====================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# ── Require admin ─────────────────────────────────────────────────────────────
|
||||
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
Write-Error "Run PowerShell as Administrator and try again."
|
||||
}
|
||||
|
||||
# ── Prompt if not provided ────────────────────────────────────────────────────
|
||||
$JarvisUrl = $JarvisUrl.TrimEnd("/")
|
||||
if (-not $Key) { $Key = Read-Host "Enter registration key" }
|
||||
|
||||
# ── Find Python 3 ─────────────────────────────────────────────────────────────
|
||||
Write-Host "Checking Python 3..." -NoNewline
|
||||
$python = $null
|
||||
$searchPaths = @(
|
||||
"python", "python3", "py",
|
||||
"$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
|
||||
"$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
|
||||
"$env:LOCALAPPDATA\Programs\Python\Python310\python.exe",
|
||||
"C:\Python312\python.exe", "C:\Python311\python.exe"
|
||||
)
|
||||
foreach ($p in $searchPaths) {
|
||||
try {
|
||||
$ver = & $p --version 2>&1
|
||||
if ("$ver" -match "Python 3") { $python = $p; break }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (-not $python) {
|
||||
Write-Host " not found." -ForegroundColor Yellow
|
||||
Write-Host "Installing Python 3.12 via winget..." -ForegroundColor Yellow
|
||||
try {
|
||||
winget install Python.Python.3.12 --silent --accept-package-agreements --accept-source-agreements
|
||||
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" +
|
||||
[System.Environment]::GetEnvironmentVariable("PATH","User")
|
||||
foreach ($p in @("python","python3")) {
|
||||
try { $ver = & $p --version 2>&1; if ("$ver" -match "Python 3") { $python = $p; break } } catch {}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
if (-not $python) { Write-Error "Python 3 not found. Install from https://python.org then re-run." }
|
||||
|
||||
# Resolve full path for task scheduler (PS 5.1 compatible — no ?. operator)
|
||||
$_pyCmd = Get-Command $python -ErrorAction SilentlyContinue
|
||||
$pythonPath = if ($_pyCmd) { $_pyCmd.Source } else { $null }
|
||||
if (-not $pythonPath -or -not (Test-Path $pythonPath)) {
|
||||
foreach ($p in @("$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
|
||||
"$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
|
||||
"C:\Python312\python.exe")) {
|
||||
if (Test-Path $p) { $pythonPath = $p; break }
|
||||
}
|
||||
}
|
||||
if (-not $pythonPath) { $pythonPath = $python }
|
||||
Write-Host " $pythonPath" -ForegroundColor Green
|
||||
|
||||
# ── Create install directory ──────────────────────────────────────────────────
|
||||
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
||||
Write-Host "Install dir: $InstallDir"
|
||||
|
||||
# ── Download Windows agent script ─────────────────────────────────────────────
|
||||
Write-Host "Downloading jarvis-agent-windows.py..." -NoNewline
|
||||
try {
|
||||
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
|
||||
$wc = New-Object System.Net.WebClient
|
||||
$wc.Headers.Add("User-Agent", "JARVIS-Installer/1.0")
|
||||
$wc.DownloadFile("$JarvisUrl/agent/jarvis-agent-windows.py", $AgentScript)
|
||||
Write-Host " done." -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Error "Download failed from $JarvisUrl/agent/jarvis-agent-windows.py`nError: $_"
|
||||
}
|
||||
|
||||
# ── Write config ──────────────────────────────────────────────────────────────
|
||||
$agentId = "${AgentName}_windows"
|
||||
$config = [ordered]@{
|
||||
jarvis_url = $JarvisUrl
|
||||
host_header = ""
|
||||
ssl_verify = $true
|
||||
registration_key = $Key
|
||||
agent_type = "windows"
|
||||
hostname = $AgentName
|
||||
agent_id = $agentId
|
||||
poll_interval = 30
|
||||
heartbeat_every = 10
|
||||
watch_services = @("WinDefend", "Spooler")
|
||||
} | ConvertTo-Json -Depth 3
|
||||
|
||||
[System.IO.File]::WriteAllText($ConfigFile, $config, [System.Text.UTF8Encoding]::new($false))
|
||||
Write-Host "Config: $ConfigFile"
|
||||
|
||||
# ── Register scheduled task ───────────────────────────────────────────────────
|
||||
try { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue } catch {}
|
||||
|
||||
$action = New-ScheduledTaskAction -Execute "`"$pythonPath`"" `
|
||||
-Argument "`"$AgentScript`"" -WorkingDirectory $InstallDir
|
||||
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Seconds 0) `
|
||||
-RestartCount 10 -RestartInterval (New-TimeSpan -Minutes 1) `
|
||||
-StartWhenAvailable -MultipleInstances IgnoreNew
|
||||
|
||||
# Run as current user (not SYSTEM) so per-user Python install is accessible
|
||||
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $currentUser -LogonType Interactive -RunLevel Highest
|
||||
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
|
||||
-Settings $settings -Principal $principal `
|
||||
-Description "JARVIS AI System Monitoring Agent" -Force | Out-Null
|
||||
|
||||
Write-Host "Scheduled task '$TaskName' registered." -ForegroundColor Green
|
||||
|
||||
# ── Start now ─────────────────────────────────────────────────────────────────
|
||||
Write-Host "Starting agent..." -NoNewline
|
||||
Start-ScheduledTask -TaskName $TaskName
|
||||
Start-Sleep -Seconds 3
|
||||
$state = (Get-ScheduledTask -TaskName $TaskName).State
|
||||
Write-Host " $state" -ForegroundColor $(if ($state -eq "Running") {"Green"} else {"Yellow"})
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " Installation complete!" -ForegroundColor Green
|
||||
Write-Host " Machine : $AgentName ($agentId)" -ForegroundColor White
|
||||
Write-Host " JARVIS : $JarvisUrl" -ForegroundColor White
|
||||
Write-Host " Logs : $InstallDir\jarvis-agent.log" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host " Useful commands:" -ForegroundColor Gray
|
||||
Write-Host " Get-Content '$InstallDir\jarvis-agent.log' -Tail 20 -Wait" -ForegroundColor Gray
|
||||
Write-Host " Stop-ScheduledTask -TaskName '$TaskName'" -ForegroundColor Gray
|
||||
Write-Host " Start-ScheduledTask -TaskName '$TaskName'" -ForegroundColor Gray
|
||||
Write-Host " Unregister-ScheduledTask -TaskName '$TaskName' -Confirm:`$false" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
# JARVIS Agent Installer
|
||||
# Usage: curl -sSL https://raw.githubusercontent.com/myronblair/jarvis/master/agent/install.sh | sudo bash
|
||||
# Or: sudo bash install.sh --jarvis-url https://jarvis.orbishosting.com --key YOUR_REGISTRATION_KEY
|
||||
|
||||
set -e
|
||||
|
||||
JARVIS_URL=""
|
||||
REG_KEY=""
|
||||
AGENT_TYPE="linux"
|
||||
INSTALL_DIR="/opt/jarvis-agent"
|
||||
CONFIG_DIR="/etc/jarvis-agent"
|
||||
STATE_DIR="/var/lib/jarvis-agent"
|
||||
SERVICE_NAME="jarvis-agent"
|
||||
|
||||
# ── Parse args ────────────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--jarvis-url) JARVIS_URL="$2"; shift 2 ;;
|
||||
--key) REG_KEY="$2"; shift 2 ;;
|
||||
--type) AGENT_TYPE="$2"; shift 2 ;;
|
||||
*) echo "Unknown arg: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Interactive prompts if not provided ──────────────────────────────────────
|
||||
if [[ -z "$JARVIS_URL" ]]; then
|
||||
read -rp "JARVIS URL (e.g. https://jarvis.orbishosting.com): " JARVIS_URL
|
||||
fi
|
||||
if [[ -z "$REG_KEY" ]]; then
|
||||
read -rp "Registration key: " REG_KEY
|
||||
fi
|
||||
|
||||
JARVIS_URL="${JARVIS_URL%/}"
|
||||
|
||||
echo ""
|
||||
echo "Installing JARVIS Agent..."
|
||||
echo " URL: $JARVIS_URL"
|
||||
echo " Type: $AGENT_TYPE"
|
||||
echo ""
|
||||
|
||||
# ── Install Python3 if needed ─────────────────────────────────────────────────
|
||||
if ! command -v python3 &>/dev/null; then
|
||||
echo "Installing python3..."
|
||||
apt-get update -qq && apt-get install -y python3 || yum install -y python3 || dnf install -y python3
|
||||
fi
|
||||
|
||||
# ── Create directories ────────────────────────────────────────────────────────
|
||||
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$STATE_DIR"
|
||||
|
||||
# ── Copy agent script ─────────────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/jarvis-agent.py" ]]; then
|
||||
cp "$SCRIPT_DIR/jarvis-agent.py" "$INSTALL_DIR/jarvis-agent.py"
|
||||
else
|
||||
echo "Downloading agent script..."
|
||||
curl -sSL "https://raw.githubusercontent.com/myronblair/jarvis/master/agent/jarvis-agent.py" \
|
||||
-o "$INSTALL_DIR/jarvis-agent.py"
|
||||
fi
|
||||
chmod +x "$INSTALL_DIR/jarvis-agent.py"
|
||||
|
||||
# ── Write config ──────────────────────────────────────────────────────────────
|
||||
HOSTNAME=$(hostname -f 2>/dev/null || hostname)
|
||||
|
||||
if [[ -f "$CONFIG_DIR/config.json" ]]; then
|
||||
echo "Config already exists at $CONFIG_DIR/config.json — skipping (keeping existing settings)."
|
||||
else
|
||||
cat > "$CONFIG_DIR/config.json" << JSONEOF
|
||||
{
|
||||
"jarvis_url": "$JARVIS_URL",
|
||||
"registration_key": "$REG_KEY",
|
||||
"hostname": "$HOSTNAME",
|
||||
"agent_type": "$AGENT_TYPE",
|
||||
"poll_interval": 30,
|
||||
"heartbeat_every": 10,
|
||||
"watch_services": ["ollama", "homeassistant", "mysql", "mariadb", "nginx", "apache2", "docker"]
|
||||
}
|
||||
JSONEOF
|
||||
chmod 600 "$CONFIG_DIR/config.json"
|
||||
echo "Config written to $CONFIG_DIR/config.json"
|
||||
fi
|
||||
|
||||
# ── Write systemd service ─────────────────────────────────────────────────────
|
||||
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << SVCEOF
|
||||
[Unit]
|
||||
Description=JARVIS Monitoring Agent
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 $INSTALL_DIR/jarvis-agent.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SVCEOF
|
||||
|
||||
# ── Enable and start ──────────────────────────────────────────────────────────
|
||||
systemctl daemon-reload
|
||||
systemctl enable "$SERVICE_NAME"
|
||||
systemctl restart "$SERVICE_NAME"
|
||||
|
||||
sleep 2
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
echo ""
|
||||
echo "✓ JARVIS Agent installed and running."
|
||||
echo " View logs: journalctl -u $SERVICE_NAME -f"
|
||||
echo " Config: $CONFIG_DIR/config.json"
|
||||
else
|
||||
echo ""
|
||||
echo "⚠ Agent installed but not running. Check logs:"
|
||||
echo " journalctl -u $SERVICE_NAME -n 30"
|
||||
fi
|
||||
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
JARVIS Agent for Windows — system monitor that reports metrics to JARVIS HUD.
|
||||
Install via PowerShell: iwr https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
|
||||
Config: C:\\ProgramData\\jarvis-agent\\config.json
|
||||
Logs: C:\\ProgramData\\jarvis-agent\\jarvis-agent.log
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
INSTALL_DIR = Path(r"C:\ProgramData\jarvis-agent")
|
||||
CONFIG_PATH = INSTALL_DIR / "config.json"
|
||||
STATE_PATH = INSTALL_DIR / "state.json"
|
||||
LOG_PATH = INSTALL_DIR / "jarvis-agent.log"
|
||||
AGENT_VERSION = "2.2"
|
||||
|
||||
# ── Logging ────────────────────────────────────────────────────────────────────
|
||||
|
||||
_log_file = None
|
||||
|
||||
def log(msg: str):
|
||||
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
line = f"[{ts}] {msg}"
|
||||
print(line, flush=True)
|
||||
try:
|
||||
with open(LOG_PATH, "a", encoding="utf-8") as f:
|
||||
f.write(line + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── Config ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def load_config() -> dict:
|
||||
if not CONFIG_PATH.exists():
|
||||
print(f"[ERROR] Config not found at {CONFIG_PATH}. Run the installer first.")
|
||||
sys.exit(1)
|
||||
with open(CONFIG_PATH, encoding="utf-8-sig") as f:
|
||||
return json.load(f)
|
||||
|
||||
def load_state() -> dict:
|
||||
if STATE_PATH.exists():
|
||||
with open(STATE_PATH, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_state(state: dict):
|
||||
INSTALL_DIR.mkdir(parents=True, exist_ok=True)
|
||||
with open(STATE_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
# ── HTTP ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
import ssl as _ssl
|
||||
|
||||
def _make_ssl_ctx(verify: bool):
|
||||
if not verify:
|
||||
ctx = _ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = _ssl.CERT_NONE
|
||||
return ctx
|
||||
return None
|
||||
|
||||
_host_header: str = ""
|
||||
|
||||
def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
|
||||
ssl_verify: bool = True) -> dict:
|
||||
body = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(url, data=body, method="POST")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/1.0")
|
||||
if _host_header:
|
||||
req.add_header("Host", _host_header)
|
||||
for k, v in headers.items():
|
||||
req.add_header(k, v)
|
||||
try:
|
||||
ctx = _make_ssl_ctx(ssl_verify)
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
||||
return json.loads(resp.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
return {"error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def api_get(url: str, headers: dict = {}, timeout: int = 10,
|
||||
ssl_verify: bool = True) -> dict:
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/1.0")
|
||||
if _host_header:
|
||||
req.add_header("Host", _host_header)
|
||||
for k, v in headers.items():
|
||||
req.add_header(k, v)
|
||||
try:
|
||||
ctx = _make_ssl_ctx(ssl_verify)
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
||||
return json.loads(resp.read().decode())
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
# ── Metrics ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _ps(script: str, timeout: int = 8) -> str:
|
||||
"""Run a PowerShell one-liner and return stdout."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
|
||||
capture_output=True, text=True, timeout=timeout
|
||||
)
|
||||
return r.stdout.strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def get_local_ip() -> str:
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(("8.8.8.8", 80))
|
||||
ip = s.getsockname()[0]
|
||||
s.close()
|
||||
return ip
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
_last_cpu_counters = None
|
||||
|
||||
def get_cpu_percent() -> float:
|
||||
global _last_cpu_counters
|
||||
try:
|
||||
out = _ps("(Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average")
|
||||
return round(float(out), 1)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
def get_memory() -> dict:
|
||||
try:
|
||||
out = _ps("$o=Get-CimInstance Win32_OperatingSystem; [PSCustomObject]@{total=$o.TotalVisibleMemorySize;free=$o.FreePhysicalMemory}|ConvertTo-Json")
|
||||
d = json.loads(out)
|
||||
total_kb = int(d.get("total", 0))
|
||||
free_kb = int(d.get("free", 0))
|
||||
used_kb = total_kb - free_kb
|
||||
if total_kb == 0:
|
||||
return {}
|
||||
return {
|
||||
"total_mb": round(total_kb / 1024, 1),
|
||||
"used_mb": round(used_kb / 1024, 1),
|
||||
"free_mb": round(free_kb / 1024, 1),
|
||||
"percent": round(used_kb / total_kb * 100, 1),
|
||||
}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def get_disk() -> list:
|
||||
try:
|
||||
out = _ps("Get-PSDrive -PSProvider FileSystem | Where-Object{$_.Used -ne $null} | Select-Object Name,@{n='used';e={[math]::Round($_.Used/1GB,2)}},@{n='free';e={[math]::Round($_.Free/1GB,2)}} | ConvertTo-Json")
|
||||
if not out:
|
||||
return []
|
||||
items = json.loads(out)
|
||||
if isinstance(items, dict):
|
||||
items = [items]
|
||||
disks = []
|
||||
for d in items:
|
||||
used = float(d.get("used", 0))
|
||||
free = float(d.get("free", 0))
|
||||
total = used + free
|
||||
pct = round(used / total * 100, 1) if total else 0
|
||||
disks.append({
|
||||
"mount": d.get("Name", "?") + ":\\",
|
||||
"size": f"{round(total, 1)}G",
|
||||
"used": f"{used}G",
|
||||
"avail": f"{free}G",
|
||||
"percent": str(int(pct)),
|
||||
})
|
||||
return disks
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def get_uptime() -> dict:
|
||||
try:
|
||||
out = _ps("(Get-Date) - (gcim Win32_OperatingSystem).LastBootUpTime | Select-Object -ExpandProperty TotalSeconds")
|
||||
secs = float(out)
|
||||
days = int(secs // 86400)
|
||||
hours = int((secs % 86400) // 3600)
|
||||
minutes = int((secs % 3600) // 60)
|
||||
return {"seconds": int(secs), "days": days, "hours": hours, "minutes": minutes,
|
||||
"human": f"{days}d {hours}h {minutes}m"}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def get_services(cfg: dict) -> list:
|
||||
watch = cfg.get("watch_services", ["WinDefend", "wuauserv", "Spooler"])
|
||||
statuses = []
|
||||
for svc in watch:
|
||||
try:
|
||||
out = _ps(f"(Get-Service -Name '{svc}' -ErrorAction SilentlyContinue).Status")
|
||||
status = "active" if out.lower() == "running" else (out.lower() or "unknown")
|
||||
statuses.append({"service": svc, "status": status})
|
||||
except Exception:
|
||||
statuses.append({"service": svc, "status": "unknown"})
|
||||
return statuses
|
||||
|
||||
def detect_capabilities(cfg: dict) -> list:
|
||||
caps = ["metrics", "commands"]
|
||||
if Path(r"C:\Program Files\Docker\Docker\Docker Desktop.exe").exists():
|
||||
caps.append("docker")
|
||||
return caps
|
||||
|
||||
def collect_metrics(cfg: dict) -> dict:
|
||||
return {
|
||||
"hostname": cfg.get("hostname", socket.gethostname()),
|
||||
"cpu_percent": get_cpu_percent(),
|
||||
"memory": get_memory(),
|
||||
"disk": get_disk(),
|
||||
"uptime": get_uptime(),
|
||||
"load": [0, 0, 0],
|
||||
"services": get_services(cfg),
|
||||
"platform": "Windows",
|
||||
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
|
||||
# ── Registration ───────────────────────────────────────────────────────────────
|
||||
|
||||
def register(cfg: dict, state: dict) -> str:
|
||||
hostname = cfg.get("hostname", socket.gethostname().lower())
|
||||
agent_type = cfg.get("agent_type", "linux")
|
||||
ip = get_local_ip()
|
||||
capabilities = detect_capabilities(cfg)
|
||||
agent_id = cfg.get("agent_id", f"{hostname}_{hostname[:8]}")
|
||||
ssl_verify = bool(cfg.get("ssl_verify", True))
|
||||
|
||||
log(f"[JARVIS] Registering as '{agent_id}' from {ip}...")
|
||||
|
||||
result = api_post(
|
||||
f"{cfg['jarvis_url']}/api/agent/register",
|
||||
{"hostname": hostname, "agent_type": agent_type, "ip_address": ip,
|
||||
"capabilities": capabilities, "agent_id": agent_id},
|
||||
headers={"X-Registration-Key": cfg["registration_key"]},
|
||||
ssl_verify=ssl_verify,
|
||||
)
|
||||
|
||||
if "error" in result:
|
||||
log(f"[ERROR] Registration failed: {result['error']}")
|
||||
return ""
|
||||
|
||||
api_key = result.get("api_key", "")
|
||||
if api_key:
|
||||
state["api_key"] = api_key
|
||||
state["agent_id"] = result.get("agent_id", agent_id)
|
||||
save_state(state)
|
||||
log(f"[JARVIS] Registered. agent_id={state['agent_id']}")
|
||||
return api_key
|
||||
|
||||
# ── Command execution ──────────────────────────────────────────────────────────
|
||||
|
||||
def execute_command(cmd: dict, cfg: dict) -> dict:
|
||||
cmd_type = cmd.get("command_type", "")
|
||||
cmd_data = cmd.get("command_data", {})
|
||||
try:
|
||||
if cmd_type == "ping":
|
||||
host = cmd_data.get("host", "8.8.8.8")
|
||||
r = subprocess.run(["ping", "-n", "3", host], capture_output=True, text=True, timeout=15)
|
||||
return {"success": r.returncode == 0, "output": r.stdout}
|
||||
|
||||
elif cmd_type == "update":
|
||||
log("[CMD] Self-update requested")
|
||||
return {"success": True, "message": "Windows agent self-update not implemented"}
|
||||
|
||||
elif cmd_type == "shell":
|
||||
if not cmd_data.get("allowed", False):
|
||||
return {"success": False, "error": "Shell commands not enabled"}
|
||||
cmd_str = cmd_data.get("command", "")
|
||||
r = subprocess.run(["powershell", "-NoProfile", "-Command", cmd_str],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
return {"success": True, "stdout": r.stdout[:2000], "stderr": r.stderr[:500]}
|
||||
|
||||
else:
|
||||
return {"success": False, "error": f"Unknown command type: {cmd_type}"}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"success": False, "error": "Command timed out"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
# ── Main loop ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
global _host_header
|
||||
|
||||
cfg = load_config()
|
||||
state = load_state()
|
||||
|
||||
jarvis_url = cfg["jarvis_url"].rstrip("/")
|
||||
ssl_verify = bool(cfg.get("ssl_verify", True))
|
||||
_host_header = cfg.get("host_header", "")
|
||||
poll_interval = int(cfg.get("poll_interval", 30))
|
||||
heartbeat_every = int(cfg.get("heartbeat_every", 10))
|
||||
|
||||
api_key = state.get("api_key", "")
|
||||
if not api_key:
|
||||
api_key = register(cfg, state)
|
||||
if not api_key:
|
||||
log("[ERROR] Could not register with JARVIS. Retrying in 60s...")
|
||||
time.sleep(60)
|
||||
main()
|
||||
return
|
||||
|
||||
headers = {"X-Agent-Key": api_key}
|
||||
last_metrics = 0
|
||||
log(f"[JARVIS] Agent v{AGENT_VERSION} (Windows) running. Connecting to {jarvis_url} every {heartbeat_every}s.")
|
||||
|
||||
while True:
|
||||
now = time.time()
|
||||
try:
|
||||
hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {}, headers, ssl_verify=ssl_verify)
|
||||
if "error" in hb:
|
||||
log(f"[WARN] Heartbeat failed: {hb['error']}")
|
||||
else:
|
||||
for cmd in hb.get("commands", []):
|
||||
log(f"[CMD] Executing: {cmd['command_type']}")
|
||||
result = execute_command(cmd, cfg)
|
||||
api_post(f"{jarvis_url}/api/agent/command_result",
|
||||
{"command_id": cmd["id"], "success": result.get("success", False), "result": result},
|
||||
headers, ssl_verify=ssl_verify)
|
||||
|
||||
if now - last_metrics >= poll_interval:
|
||||
metrics = collect_metrics(cfg)
|
||||
r = api_post(f"{jarvis_url}/api/agent/metrics",
|
||||
{"system": metrics}, headers, ssl_verify=ssl_verify)
|
||||
if "error" not in r:
|
||||
last_metrics = now
|
||||
|
||||
except Exception as e:
|
||||
log(f"[ERROR] Loop error: {e}")
|
||||
|
||||
time.sleep(heartbeat_every)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,454 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
JARVIS Agent — lightweight system monitor for Linux machines.
|
||||
Registers with JARVIS, reports metrics, and executes commands.
|
||||
|
||||
Install: sudo bash /opt/jarvis-agent/install.sh
|
||||
Config: /etc/jarvis-agent/config.json
|
||||
Logs: journalctl -u jarvis-agent -f
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG_PATH = "/etc/jarvis-agent/config.json"
|
||||
STATE_PATH = "/var/lib/jarvis-agent/state.json"
|
||||
AGENT_VERSION = "2.3" # bumped on each release
|
||||
|
||||
# ── Config helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
def load_config() -> dict:
|
||||
if not os.path.exists(CONFIG_PATH):
|
||||
print(f"[ERROR] Config not found at {CONFIG_PATH}. Run the installer first.", flush=True)
|
||||
sys.exit(1)
|
||||
with open(CONFIG_PATH) as f:
|
||||
return json.load(f)
|
||||
|
||||
def load_state() -> dict:
|
||||
if os.path.exists(STATE_PATH):
|
||||
with open(STATE_PATH) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_state(state: dict):
|
||||
Path(STATE_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(STATE_PATH, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
# ── HTTP helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
import ssl as _ssl
|
||||
|
||||
def _make_ssl_ctx(verify: bool) -> _ssl.SSLContext | None:
|
||||
if not verify:
|
||||
ctx = _ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = _ssl.CERT_NONE
|
||||
return ctx
|
||||
return None
|
||||
|
||||
_host_header: str = "" # set from config at startup
|
||||
|
||||
def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
|
||||
ssl_verify: bool = True) -> dict:
|
||||
body = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(url, data=body, method="POST")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
req.add_header("User-Agent", "JARVIS-Agent/1.0")
|
||||
if _host_header:
|
||||
req.add_header("Host", _host_header)
|
||||
for k, v in headers.items():
|
||||
req.add_header(k, v)
|
||||
try:
|
||||
ctx = _make_ssl_ctx(ssl_verify)
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
||||
return json.loads(resp.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
return {"error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def api_get(url: str, headers: dict = {}, timeout: int = 10,
|
||||
ssl_verify: bool = True) -> dict:
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("User-Agent", "JARVIS-Agent/1.0")
|
||||
if _host_header:
|
||||
req.add_header("Host", _host_header)
|
||||
for k, v in headers.items():
|
||||
req.add_header(k, v)
|
||||
try:
|
||||
ctx = _make_ssl_ctx(ssl_verify)
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
||||
return json.loads(resp.read().decode())
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
# ── Registration ──────────────────────────────────────────────────────────────
|
||||
|
||||
def get_local_ip() -> str:
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(("8.8.8.8", 80))
|
||||
ip = s.getsockname()[0]
|
||||
s.close()
|
||||
return ip
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
def detect_capabilities(cfg: dict) -> list:
|
||||
caps = ["metrics", "commands"]
|
||||
# Check for Proxmox
|
||||
if os.path.exists("/usr/bin/pvesh") or os.path.exists("/usr/sbin/pveversion"):
|
||||
caps.append("proxmox")
|
||||
# Check for Docker
|
||||
if os.path.exists("/usr/bin/docker") or os.path.exists("/usr/local/bin/docker"):
|
||||
caps.append("docker")
|
||||
# Check for Ollama
|
||||
if os.path.exists("/usr/local/bin/ollama") or os.path.exists("/usr/bin/ollama"):
|
||||
caps.append("ollama")
|
||||
# Check for Home Assistant
|
||||
if os.path.exists("/etc/homeassistant") or os.path.exists("/config/configuration.yaml"):
|
||||
caps.append("homeassistant")
|
||||
return caps
|
||||
|
||||
def register(cfg: dict, state: dict) -> str:
|
||||
"""Register with JARVIS. Returns api_key."""
|
||||
hostname = cfg.get("hostname", socket.gethostname())
|
||||
agent_type = cfg.get("agent_type", "linux")
|
||||
ip = get_local_ip()
|
||||
capabilities = detect_capabilities(cfg)
|
||||
agent_id = cfg.get("agent_id", f"{hostname}_{socket.gethostname()[:8]}")
|
||||
ssl_verify = bool(cfg.get("ssl_verify", True))
|
||||
|
||||
print(f"[JARVIS] Registering as '{agent_id}' ({agent_type}) from {ip}...", flush=True)
|
||||
|
||||
result = api_post(
|
||||
f"{cfg['jarvis_url']}/api/agent/register",
|
||||
{
|
||||
"hostname": hostname,
|
||||
"agent_type": agent_type,
|
||||
"ip_address": ip,
|
||||
"capabilities": capabilities,
|
||||
"agent_id": agent_id,
|
||||
},
|
||||
headers={"X-Registration-Key": cfg["registration_key"]},
|
||||
ssl_verify=ssl_verify,
|
||||
)
|
||||
|
||||
if "error" in result:
|
||||
print(f"[ERROR] Registration failed: {result['error']}", flush=True)
|
||||
return ""
|
||||
|
||||
api_key = result.get("api_key", "")
|
||||
if api_key:
|
||||
state["api_key"] = api_key
|
||||
state["agent_id"] = result.get("agent_id", agent_id)
|
||||
save_state(state)
|
||||
print(f"[JARVIS] Registered. agent_id={state['agent_id']}", flush=True)
|
||||
return api_key
|
||||
|
||||
# ── Metrics collection ────────────────────────────────────────────────────────
|
||||
|
||||
def read_cpu_percent() -> float:
|
||||
try:
|
||||
with open("/proc/stat") as f:
|
||||
line = f.readline()
|
||||
fields = list(map(int, line.split()[1:]))
|
||||
idle = fields[3]
|
||||
total = sum(fields)
|
||||
return round((1 - idle / total) * 100, 1) if total else 0.0
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
_last_cpu = None
|
||||
|
||||
def get_cpu_percent() -> float:
|
||||
global _last_cpu
|
||||
try:
|
||||
with open("/proc/stat") as f:
|
||||
line = f.readline()
|
||||
fields = list(map(int, line.split()[1:]))
|
||||
idle = fields[3] + fields[4] # idle + iowait
|
||||
total = sum(fields)
|
||||
if _last_cpu:
|
||||
d_idle = idle - _last_cpu[0]
|
||||
d_total = total - _last_cpu[1]
|
||||
result = round((1 - d_idle / d_total) * 100, 1) if d_total else 0.0
|
||||
else:
|
||||
result = 0.0
|
||||
_last_cpu = (idle, total)
|
||||
return result
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
def get_memory() -> dict:
|
||||
mem = {}
|
||||
try:
|
||||
with open("/proc/meminfo") as f:
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if parts[0] in ("MemTotal:", "MemAvailable:", "MemFree:", "Buffers:", "Cached:"):
|
||||
mem[parts[0].rstrip(":")] = int(parts[1])
|
||||
total = mem.get("MemTotal", 0)
|
||||
available = mem.get("MemAvailable", 0)
|
||||
used = total - available
|
||||
return {
|
||||
"total_mb": round(total / 1024, 1),
|
||||
"used_mb": round(used / 1024, 1),
|
||||
"free_mb": round(available / 1024, 1),
|
||||
"percent": round(used / total * 100, 1) if total else 0,
|
||||
}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def get_disk() -> list:
|
||||
disks = []
|
||||
try:
|
||||
result = subprocess.run(["df", "-h", "--output=source,fstype,size,used,avail,pcent,target"],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
lines = result.stdout.strip().split("\n")[1:]
|
||||
for line in lines:
|
||||
parts = line.split()
|
||||
if len(parts) >= 7:
|
||||
mount = parts[6]
|
||||
if not any(mount.startswith(x) for x in ["/sys", "/proc", "/dev/pts", "/run", "/snap"]):
|
||||
disks.append({
|
||||
"mount": mount,
|
||||
"size": parts[2],
|
||||
"used": parts[3],
|
||||
"avail": parts[4],
|
||||
"percent": parts[5].rstrip("%"),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return disks
|
||||
|
||||
def get_uptime() -> dict:
|
||||
try:
|
||||
with open("/proc/uptime") as f:
|
||||
secs = float(f.read().split()[0])
|
||||
days = int(secs // 86400)
|
||||
hours = int((secs % 86400) // 3600)
|
||||
minutes = int((secs % 3600) // 60)
|
||||
return {"seconds": int(secs), "days": days, "hours": hours, "minutes": minutes,
|
||||
"human": f"{days}d {hours}h {minutes}m"}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def get_services(cfg: dict) -> list:
|
||||
watch = cfg.get("watch_services", ["ollama", "homeassistant", "mysql", "nginx", "apache2"])
|
||||
statuses = []
|
||||
for svc in watch:
|
||||
try:
|
||||
r = subprocess.run(["systemctl", "is-active", svc], capture_output=True, text=True, timeout=3)
|
||||
statuses.append({"service": svc, "status": r.stdout.strip()})
|
||||
except Exception:
|
||||
statuses.append({"service": svc, "status": "unknown"})
|
||||
return statuses
|
||||
|
||||
def get_load() -> list:
|
||||
try:
|
||||
with open("/proc/loadavg") as f:
|
||||
parts = f.read().split()
|
||||
return [float(parts[0]), float(parts[1]), float(parts[2])]
|
||||
except Exception:
|
||||
return [0, 0, 0]
|
||||
|
||||
def collect_metrics(cfg: dict) -> dict:
|
||||
# First reading for CPU delta
|
||||
get_cpu_percent()
|
||||
time.sleep(1)
|
||||
return {
|
||||
"hostname": cfg.get("hostname", socket.gethostname()),
|
||||
"cpu_percent": get_cpu_percent(),
|
||||
"memory": get_memory(),
|
||||
"disk": get_disk(),
|
||||
"uptime": get_uptime(),
|
||||
"load": get_load(),
|
||||
"services": get_services(cfg),
|
||||
"platform": platform.system(),
|
||||
"timestamp": datetime.utcnow().isoformat() + "Z",
|
||||
}
|
||||
|
||||
# ── Proxmox metrics ───────────────────────────────────────────────────────────
|
||||
|
||||
def collect_proxmox_metrics(cfg: dict) -> dict | None:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pvesh", "get", "/nodes/pve/status", "--output-format", "json"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
node_status = json.loads(result.stdout)
|
||||
vms_result = subprocess.run(
|
||||
["pvesh", "get", "/nodes/pve/qemu", "--output-format", "json"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
vms = json.loads(vms_result.stdout)
|
||||
return {"node": node_status, "vms": vms}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
# ── Command execution ─────────────────────────────────────────────────────────
|
||||
|
||||
def execute_command(cmd: dict) -> dict:
|
||||
cmd_type = cmd.get("command_type", "")
|
||||
cmd_data = cmd.get("command_data", {})
|
||||
|
||||
try:
|
||||
if cmd_type == "restart_service":
|
||||
svc = cmd_data.get("service", "")
|
||||
if not svc or "/" in svc:
|
||||
return {"success": False, "error": "Invalid service name"}
|
||||
r = subprocess.run(["systemctl", "restart", svc], capture_output=True, text=True, timeout=30)
|
||||
return {"success": r.returncode == 0, "stdout": r.stdout, "stderr": r.stderr}
|
||||
|
||||
elif cmd_type == "get_logs":
|
||||
svc = cmd_data.get("service", "")
|
||||
lines = min(int(cmd_data.get("lines", 50)), 200)
|
||||
if not svc or "/" in svc:
|
||||
return {"success": False, "error": "Invalid service name"}
|
||||
r = subprocess.run(["journalctl", "-u", svc, "-n", str(lines), "--no-pager"],
|
||||
capture_output=True, text=True, timeout=15)
|
||||
return {"success": True, "output": r.stdout}
|
||||
|
||||
elif cmd_type == "ping":
|
||||
host = cmd_data.get("host", "8.8.8.8")
|
||||
r = subprocess.run(["ping", "-c", "3", "-W", "2", host], capture_output=True, text=True, timeout=15)
|
||||
return {"success": r.returncode == 0, "output": r.stdout}
|
||||
|
||||
elif cmd_type == "update":
|
||||
updated = self_update(cfg)
|
||||
return {"success": True, "updated": updated}
|
||||
|
||||
elif cmd_type == "shell":
|
||||
# Only allow if explicitly enabled in config
|
||||
if not cmd_data.get("allowed", False):
|
||||
return {"success": False, "error": "Shell commands not enabled"}
|
||||
cmd_str = cmd_data.get("command", "")
|
||||
r = subprocess.run(cmd_str, shell=True, capture_output=True, text=True, timeout=30)
|
||||
return {"success": True, "stdout": r.stdout[:2000], "stderr": r.stderr[:500]}
|
||||
|
||||
else:
|
||||
return {"success": False, "error": f"Unknown command type: {cmd_type}"}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"success": False, "error": "Command timed out"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
# ── Main loop ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
global _host_header
|
||||
cfg = load_config()
|
||||
state = load_state()
|
||||
|
||||
jarvis_url = cfg["jarvis_url"].rstrip("/")
|
||||
ssl_verify = bool(cfg.get("ssl_verify", True))
|
||||
_host_header = cfg.get("host_header", "")
|
||||
poll_interval = int(cfg.get("poll_interval", 30))
|
||||
heartbeat_every = int(cfg.get("heartbeat_every", 10))
|
||||
|
||||
# Register if no API key yet
|
||||
api_key = state.get("api_key", "")
|
||||
if not api_key:
|
||||
api_key = register(cfg, state)
|
||||
if not api_key:
|
||||
print("[ERROR] Could not register with JARVIS. Retrying in 60s...", flush=True)
|
||||
time.sleep(60)
|
||||
main()
|
||||
return
|
||||
|
||||
headers = {"X-Agent-Key": api_key}
|
||||
last_metrics = 0
|
||||
last_update_chk = 0
|
||||
update_interval = int(cfg.get("update_check_hours", 24)) * 3600
|
||||
tick = 0
|
||||
|
||||
print(f"[JARVIS] Agent v{AGENT_VERSION} running. Polling {jarvis_url} every {heartbeat_every}s.", flush=True)
|
||||
|
||||
while True:
|
||||
tick += 1
|
||||
now = time.time()
|
||||
|
||||
try:
|
||||
# Heartbeat + get commands
|
||||
hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {}, headers, ssl_verify=ssl_verify)
|
||||
if "error" in hb:
|
||||
print(f"[WARN] Heartbeat failed: {hb['error']}", flush=True)
|
||||
else:
|
||||
commands = hb.get("commands", [])
|
||||
for cmd in commands:
|
||||
print(f"[CMD] Executing: {cmd['command_type']}", flush=True)
|
||||
result = execute_command(cmd)
|
||||
api_post(f"{jarvis_url}/api/agent/command_result",
|
||||
{"command_id": cmd["id"], "success": result.get("success", False), "result": result},
|
||||
headers, ssl_verify=ssl_verify)
|
||||
|
||||
# Self-update check (every update_interval seconds, default 24h)
|
||||
if now - last_update_chk >= update_interval:
|
||||
last_update_chk = now
|
||||
self_update(cfg) # restarts process if update found
|
||||
|
||||
# Push metrics every poll_interval seconds
|
||||
if now - last_metrics >= poll_interval:
|
||||
metrics = collect_metrics(cfg)
|
||||
api_post(f"{jarvis_url}/api/agent/metrics",
|
||||
{"type": "system", "data": metrics}, headers, ssl_verify=ssl_verify)
|
||||
|
||||
# Proxmox metrics if available
|
||||
if "proxmox" in detect_capabilities(cfg):
|
||||
px = collect_proxmox_metrics(cfg)
|
||||
if px:
|
||||
api_post(f"{jarvis_url}/api/agent/metrics",
|
||||
{"type": "proxmox", "data": px}, headers, ssl_verify=ssl_verify)
|
||||
|
||||
last_metrics = now
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Loop error: {e}", flush=True)
|
||||
|
||||
time.sleep(heartbeat_every)
|
||||
|
||||
|
||||
# ── Self-update ────────────────────────────────────────────────────────────────
|
||||
|
||||
def self_update(cfg: dict) -> bool:
|
||||
"""Check JARVIS server for a newer version of this script. If different, replace and restart."""
|
||||
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
|
||||
default_update_url = f"{jarvis_url}/agent/jarvis-agent.py" if jarvis_url else ""
|
||||
update_url = cfg.get("update_url", default_update_url)
|
||||
if not update_url:
|
||||
return False
|
||||
script_path = os.path.abspath(__file__)
|
||||
try:
|
||||
req = urllib.request.Request(update_url)
|
||||
req.add_header("User-Agent", "JARVIS-Agent/1.0")
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
new_content = resp.read()
|
||||
with open(script_path, "rb") as f:
|
||||
current = f.read()
|
||||
if new_content != current:
|
||||
print(f"[JARVIS] Update available — replacing {script_path} and restarting...", flush=True)
|
||||
with open(script_path, "wb") as f:
|
||||
f.write(new_content)
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"[JARVIS] Self-update check failed: {e}", flush=True)
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,36 @@
|
||||
# Kill any stale Task Scheduler approach
|
||||
Unregister-ScheduledTask -TaskName 'JARVIS-Agent' -Confirm:$false -ErrorAction SilentlyContinue
|
||||
|
||||
# Create a VBScript launcher (runs Python silently, no console window)
|
||||
$vbs = 'Set WShell = CreateObject("WScript.Shell")' + "`r`n" +
|
||||
'WShell.Run """C:\Users\myron\AppData\Local\Programs\Python\Python312\pythonw.exe"" ""C:\ProgramData\jarvis-agent\jarvis-agent.py""", 0, False'
|
||||
|
||||
[System.IO.File]::WriteAllText('C:\ProgramData\jarvis-agent\start-agent.vbs', $vbs, [System.Text.ASCIIEncoding]::new())
|
||||
|
||||
# Add to user startup folder so it runs at every login
|
||||
$startupDir = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"
|
||||
Copy-Item 'C:\ProgramData\jarvis-agent\start-agent.vbs' "$startupDir\JARVIS-Agent.vbs" -Force
|
||||
Write-Host "Startup entry created: $startupDir\JARVIS-Agent.vbs" -ForegroundColor Green
|
||||
|
||||
# Kill any existing python process running the agent
|
||||
Get-Process python*, pythonw* -ErrorAction SilentlyContinue | Where-Object {$_.CommandLine -like '*jarvis-agent*'} | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Launch now
|
||||
Write-Host "Starting agent..." -ForegroundColor Cyan
|
||||
Start-Process 'C:\Users\myron\AppData\Local\Programs\Python\Python312\pythonw.exe' -ArgumentList 'C:\ProgramData\jarvis-agent\jarvis-agent.py' -WorkingDirectory 'C:\ProgramData\jarvis-agent'
|
||||
Start-Sleep -Seconds 4
|
||||
|
||||
# Confirm running
|
||||
$proc = Get-Process pythonw -ErrorAction SilentlyContinue
|
||||
if ($proc) {
|
||||
Write-Host "Agent running — PID $($proc.Id)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "pythonw not found — check C:\ProgramData\jarvis-agent\jarvis-agent.log" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Show log tail
|
||||
Start-Sleep -Seconds 2
|
||||
if (Test-Path 'C:\ProgramData\jarvis-agent\jarvis-agent.log') {
|
||||
Write-Host "`nLog:" -ForegroundColor Cyan
|
||||
Get-Content 'C:\ProgramData\jarvis-agent\jarvis-agent.log' -Tail 10
|
||||
}
|
||||
Reference in New Issue
Block a user