diff --git a/agent/install-mac.sh b/agent/install-mac.sh
new file mode 100644
index 0000000..9f5fda0
--- /dev/null
+++ b/agent/install-mac.sh
@@ -0,0 +1,135 @@
+#!/bin/bash
+# JARVIS Agent Installer — macOS
+# Usage: bash install-mac.sh --jarvis-url https://jarvis.orbishosting.com --key YOUR_KEY
+# Or one-liner: bash <(curl -sSL https://jarvis.orbishosting.com/agent/install-mac.sh) --key YOUR_KEY
+
+set -e
+
+JARVIS_URL="https://jarvis.orbishosting.com"
+REG_KEY=""
+INSTALL_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 "$REG_KEY" ]]; then
+ read -rp "Registration key: " REG_KEY
+fi
+
+JARVIS_URL="${JARVIS_URL%/}"
+
+echo ""
+echo " ===================================="
+echo " JARVIS Agent Installer v3.0 "
+echo " ===================================="
+echo ""
+
+# Check for Python3
+PYTHON3=$(command -v 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 2>&1))"
+
+mkdir -p "$INSTALL_DIR"
+
+# Download macOS-native agent script
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || echo "")"
+if [[ -f "$SCRIPT_DIR/jarvis-agent-mac.py" ]]; then
+ cp "$SCRIPT_DIR/jarvis-agent-mac.py" "$INSTALL_DIR/jarvis-agent.py"
+ echo "Copied agent from local directory."
+else
+ echo "Downloading macOS agent..."
+ curl -sSL "$JARVIS_URL/agent/jarvis-agent-mac.py" -o "$INSTALL_DIR/jarvis-agent.py"
+ echo "Downloaded."
+fi
+chmod +x "$INSTALL_DIR/jarvis-agent.py"
+
+HOSTNAME=$(hostname -f 2>/dev/null || hostname)
+AGENT_ID="${HOSTNAME}_mac"
+
+# Write config (preserve existing)
+if [[ ! -f "$INSTALL_DIR/config.json" ]]; then
+cat > "$INSTALL_DIR/config.json" << JSONEOF
+{
+ "jarvis_url": "$JARVIS_URL",
+ "registration_key": "$REG_KEY",
+ "hostname": "$HOSTNAME",
+ "agent_id": "$AGENT_ID",
+ "agent_type": "macos",
+ "poll_interval": 30,
+ "heartbeat_every": 10,
+ "update_check_hours": 24,
+ "watch_services": [],
+ "allow_shell_commands": false
+}
+JSONEOF
+ chmod 600 "$INSTALL_DIR/config.json"
+ echo "Config written to $INSTALL_DIR/config.json"
+else
+ echo "Config already exists — preserving $INSTALL_DIR/config.json"
+fi
+
+# Write launchd plist
+mkdir -p "$HOME/Library/LaunchAgents"
+cat > "$PLIST_PATH" << PLISTEOF
+
+
+
+
+ Label
+ $SERVICE_LABEL
+ ProgramArguments
+
+ $PYTHON3
+ $INSTALL_DIR/jarvis-agent.py
+
+ EnvironmentVariables
+
+ JARVIS_CONFIG
+ $INSTALL_DIR/config.json
+ JARVIS_STATE
+ $INSTALL_DIR/state.json
+
+ RunAtLoad
+
+ KeepAlive
+
+ StandardOutPath
+ $INSTALL_DIR/jarvis-agent.log
+ StandardErrorPath
+ $INSTALL_DIR/jarvis-agent.log
+
+
+PLISTEOF
+
+# Load/reload the service
+launchctl unload "$PLIST_PATH" 2>/dev/null || true
+launchctl load "$PLIST_PATH"
+
+sleep 2
+if launchctl list 2>/dev/null | grep -q "$SERVICE_LABEL"; then
+ echo ""
+ echo " JARVIS Agent installed and running."
+ echo " Machine : $HOSTNAME ($AGENT_ID)"
+ echo " JARVIS : $JARVIS_URL"
+ echo " Logs : tail -f $INSTALL_DIR/jarvis-agent.log"
+ echo " Config : $INSTALL_DIR/config.json"
+ echo " Stop : launchctl unload $PLIST_PATH"
+ echo " Update : curl -sSL $JARVIS_URL/agent/install-mac.sh | bash -s -- --key $REG_KEY"
+else
+ echo ""
+ echo " Agent installed but not detected as running. Check logs:"
+ echo " tail -f $INSTALL_DIR/jarvis-agent.log"
+fi
+echo ""
diff --git a/agent/jarvis-agent-mac.py b/agent/jarvis-agent-mac.py
new file mode 100644
index 0000000..6bd487a
--- /dev/null
+++ b/agent/jarvis-agent-mac.py
@@ -0,0 +1,576 @@
+#!/usr/bin/env python3
+"""
+JARVIS Agent for macOS — system monitor that reports metrics to JARVIS HUD.
+Install: bash <(curl -sSL https://jarvis.orbishosting.com/agent/install-mac.sh) --key YOUR_KEY
+Config: ~/.jarvis-agent/config.json (or $JARVIS_CONFIG)
+Logs: ~/.jarvis-agent/jarvis-agent.log (or journalctl via launchd)
+"""
+
+import json
+import os
+import platform
+import re
+import socket
+import subprocess
+import sys
+import time
+import urllib.request
+import urllib.error
+import hashlib
+from datetime import datetime
+from pathlib import Path
+
+AGENT_VERSION = "3.0"
+
+# Config/state paths — env vars let the launchd plist override them
+_default_dir = Path.home() / ".jarvis-agent"
+CONFIG_PATH = Path(os.environ.get("JARVIS_CONFIG", str(_default_dir / "config.json")))
+STATE_PATH = Path(os.environ.get("JARVIS_STATE", str(_default_dir / "state.json")))
+LOG_PATH = _default_dir / "jarvis-agent.log"
+
+# ── Logging ────────────────────────────────────────────────────────────────────
+
+def log(msg: str):
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ line = f"[{ts}] {msg}"
+ print(line, flush=True)
+ try:
+ LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
+ with open(LOG_PATH, "a") 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) as f:
+ return json.load(f)
+
+def load_state() -> dict:
+ if STATE_PATH.exists():
+ with open(STATE_PATH) as f:
+ return json.load(f)
+ return {}
+
+def save_state(state: dict):
+ STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
+ with open(STATE_PATH, "w") 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-macOS/3.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-macOS/3.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 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_times = None
+
+def get_cpu_percent() -> float:
+ """Delta-based CPU % using top (two samples)."""
+ global _last_cpu_times
+ try:
+ # top -l 2 gives two snapshots; second line has the real delta
+ r = subprocess.run(
+ ["top", "-l", "2", "-s", "0", "-n", "0"],
+ capture_output=True, text=True, timeout=12
+ )
+ idle = None
+ for line in r.stdout.splitlines():
+ if "CPU usage:" in line:
+ m = re.search(r"([\d.]+)%\s+idle", line)
+ if m:
+ idle = float(m.group(1))
+ if idle is not None:
+ return round(100.0 - idle, 1)
+ except Exception:
+ pass
+ return 0.0
+
+def get_memory() -> dict:
+ try:
+ # Total physical memory
+ r = subprocess.run(["sysctl", "-n", "hw.memsize"], capture_output=True, text=True, timeout=3)
+ total_bytes = int(r.stdout.strip())
+
+ # vm_stat for page counts; default page size on Apple Silicon and Intel = 4096
+ page_size = 4096
+ try:
+ ps_r = subprocess.run(["sysctl", "-n", "hw.pagesize"], capture_output=True, text=True, timeout=3)
+ page_size = int(ps_r.stdout.strip())
+ except Exception:
+ pass
+
+ vm_r = subprocess.run(["vm_stat"], capture_output=True, text=True, timeout=5)
+ pages = {}
+ for line in vm_r.stdout.splitlines():
+ m = re.match(r"Pages\s+(.+?):\s+([\d]+)", line)
+ if m:
+ pages[m.group(1).strip().lower()] = int(m.group(2))
+
+ free_pages = pages.get("free", 0) + pages.get("speculative", 0)
+ # "available" = free + inactive (can be reclaimed)
+ avail_pages = free_pages + pages.get("inactive", 0)
+ used_pages = (total_bytes // page_size) - avail_pages
+
+ total_mb = round(total_bytes / (1024 * 1024), 1)
+ used_mb = round(used_pages * page_size / (1024 * 1024), 1)
+ avail_mb = round(avail_pages * page_size / (1024 * 1024), 1)
+ used_mb = max(0, min(used_mb, total_mb))
+
+ return {
+ "total_mb": total_mb,
+ "used_mb": used_mb,
+ "free_mb": avail_mb,
+ "percent": round(used_mb / total_mb * 100, 1) if total_mb else 0,
+ }
+ except Exception:
+ return {}
+
+def get_disk() -> list:
+ disks = []
+ try:
+ r = subprocess.run(["df", "-H", "-l"], capture_output=True, text=True, timeout=5)
+ for line in r.stdout.splitlines()[1:]:
+ parts = line.split()
+ if len(parts) >= 6:
+ mount = parts[5]
+ if not any(mount.startswith(x) for x in ["/dev", "/private/var/vm", "/System/Volumes/VM"]):
+ disks.append({
+ "mount": mount,
+ "size": parts[1],
+ "used": parts[2],
+ "avail": parts[3],
+ "percent": parts[4].rstrip("%"),
+ })
+ except Exception:
+ pass
+ return disks
+
+def get_uptime() -> dict:
+ try:
+ r = subprocess.run(["sysctl", "-n", "kern.boottime"], capture_output=True, text=True, timeout=3)
+ m = re.search(r"sec\s*=\s*(\d+)", r.stdout)
+ if m:
+ boot_ts = int(m.group(1))
+ secs = time.time() - boot_ts
+ 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:
+ pass
+ return {}
+
+def get_load() -> list:
+ try:
+ r = subprocess.run(["sysctl", "-n", "vm.loadavg"], capture_output=True, text=True, timeout=3)
+ # format: "{ 0.12 0.34 0.56 }"
+ nums = re.findall(r"[\d.]+", r.stdout)
+ if len(nums) >= 3:
+ return [float(nums[0]), float(nums[1]), float(nums[2])]
+ except Exception:
+ pass
+ return [0, 0, 0]
+
+def get_services(cfg: dict) -> list:
+ """Check macOS LaunchDaemon/LaunchAgent services via launchctl."""
+ watch = cfg.get("watch_services", [])
+ if not watch:
+ return []
+ statuses = []
+ try:
+ r = subprocess.run(["launchctl", "list"], capture_output=True, text=True, timeout=5)
+ running = set()
+ for line in r.stdout.splitlines():
+ parts = line.split()
+ if len(parts) >= 3:
+ running.add(parts[2])
+ except Exception:
+ running = set()
+ for svc in watch:
+ status = "active" if any(svc in s for s in running) else "inactive"
+ statuses.append({"service": svc, "status": status})
+ return statuses
+
+def detect_capabilities(cfg: dict) -> list:
+ import shutil
+ caps = ["metrics", "commands"]
+ if shutil.which("docker"):
+ caps.append("docker")
+ if shutil.which("ollama"):
+ caps.append("ollama")
+ # screencapture is always available on macOS
+ caps.append("screenshot")
+ caps.append("sysinfo")
+ 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": get_load(),
+ "services": get_services(cfg),
+ "platform": "macOS",
+ "os_version": platform.mac_ver()[0],
+ "timestamp": datetime.utcnow().isoformat() + "Z",
+ }
+
+# ── Registration ───────────────────────────────────────────────────────────────
+
+def register(cfg: dict, state: dict) -> str:
+ hostname = cfg.get("hostname", socket.gethostname())
+ agent_type = cfg.get("agent_type", "macos")
+ ip = get_local_ip()
+ capabilities = detect_capabilities(cfg)
+ agent_id = cfg.get("agent_id", f"{hostname}_mac")
+ ssl_verify = bool(cfg.get("ssl_verify", True))
+
+ log(f"Registering as '{agent_id}' ({agent_type}) 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"Registered. agent_id={state['agent_id']}")
+ return api_key
+
+# ── Screenshot ─────────────────────────────────────────────────────────────────
+
+def _take_screenshot(cmd_data: dict) -> dict:
+ import base64, tempfile, shutil
+ tmp = tempfile.mktemp(suffix=".png")
+ method = "unknown"
+
+ # screencapture -x = no sound, always available on macOS
+ try:
+ r = subprocess.run(["screencapture", "-x", "-t", "png", tmp],
+ capture_output=True, timeout=15)
+ if r.returncode == 0 and os.path.exists(tmp):
+ method = "screencapture"
+ except Exception:
+ pass
+
+ # Fallback: PIL
+ if method == "unknown":
+ try:
+ from PIL import ImageGrab
+ img = ImageGrab.grab()
+ img.save(tmp, "PNG")
+ method = "pil"
+ except Exception:
+ pass
+
+ if method == "unknown" or not os.path.exists(tmp):
+ snap = _sysinfo_snapshot()
+ snap["screenshot_available"] = False
+ snap["method"] = "text_only"
+ return snap
+
+ try:
+ with open(tmp, "rb") as f:
+ raw = f.read()
+ b64 = base64.b64encode(raw).decode()
+ try:
+ os.unlink(tmp)
+ except Exception:
+ pass
+ return {
+ "success": True,
+ "method": method,
+ "image_b64": b64,
+ "image_mime": "image/png",
+ "file_size": len(raw),
+ "hostname": socket.gethostname(),
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e), "method": method}
+
+# ── Sysinfo ────────────────────────────────────────────────────────────────────
+
+def _sysinfo_snapshot() -> dict:
+ data = {"success": True, "hostname": socket.gethostname(),
+ "snapshot_type": "text", "screenshot_available": False, "platform": "macOS"}
+ try:
+ mem = get_memory()
+ data["mem_total_mb"] = int(mem.get("total_mb", 0))
+ data["mem_used_mb"] = int(mem.get("used_mb", 0))
+ data["mem_avail_mb"] = int(mem.get("free_mb", 0))
+ except Exception:
+ pass
+ try:
+ load = get_load()
+ data["load_1m"], data["load_5m"], data["load_15m"] = load[0], load[1], load[2]
+ except Exception:
+ pass
+ try:
+ r = subprocess.run(["df", "-H", "/"], capture_output=True, text=True, timeout=5)
+ data["disk"] = r.stdout.splitlines()[1] if r.stdout else ""
+ except Exception:
+ pass
+ try:
+ r = subprocess.run(["ps", "aux", "-r"], capture_output=True, text=True, timeout=5)
+ data["top_procs"] = r.stdout.splitlines()[1:8]
+ except Exception:
+ pass
+ try:
+ r = subprocess.run(["netstat", "-an", "-p", "tcp"], capture_output=True, text=True, timeout=5)
+ listening = [l for l in r.stdout.splitlines() if "LISTEN" in l]
+ data["listening_ports"] = "\n".join(listening[:20])
+ except Exception:
+ pass
+ return data
+
+# ── Command execution ──────────────────────────────────────────────────────────
+
+def execute_command(cmd: 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", "-c", "3", "-W", "2000", host],
+ capture_output=True, text=True, timeout=15)
+ return {"success": r.returncode == 0, "output": r.stdout}
+
+ elif cmd_type == "update":
+ _cfg = load_config()
+ updated = self_update(_cfg)
+ return {"success": True, "updated": updated}
+
+ elif cmd_type == "shell":
+ _cfg = load_config()
+ if not _cfg.get("allow_shell_commands", False):
+ return {"success": False, "error": "Shell commands not enabled in agent config"}
+ 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]}
+
+ elif 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(["launchctl", "kickstart", "-k", f"system/{svc}"],
+ capture_output=True, text=True, timeout=15)
+ if r.returncode != 0:
+ r = subprocess.run(["brew", "services", "restart", svc],
+ capture_output=True, text=True, timeout=15)
+ 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)
+ r = subprocess.run(["log", "show", "--predicate", f'process == "{svc}"',
+ "--last", "1h", "--style", "compact"],
+ capture_output=True, text=True, timeout=15)
+ output = "\n".join(r.stdout.splitlines()[-lines:])
+ return {"success": True, "output": output}
+
+ elif cmd_type == "screenshot":
+ return _take_screenshot(cmd_data)
+
+ elif cmd_type == "sysinfo":
+ return _sysinfo_snapshot()
+
+ 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)}
+
+# ── Self-update ────────────────────────────────────────────────────────────────
+
+def self_update(cfg: dict) -> bool:
+ jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
+ default_update_url = f"{jarvis_url}/agent/jarvis-agent-mac.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__)
+ ssl_verify = bool(cfg.get("ssl_verify", True))
+ try:
+ # Download expected hash
+ hash_url = update_url + ".sha256"
+ req_hash = urllib.request.Request(hash_url)
+ req_hash.add_header("User-Agent", "JARVIS-Agent-macOS/3.0")
+ if _host_header:
+ req_hash.add_header("Host", _host_header)
+ expected_hash = None
+ try:
+ ctx = _make_ssl_ctx(ssl_verify)
+ with urllib.request.urlopen(req_hash, timeout=10, context=ctx) as resp:
+ expected_hash = resp.read().decode().strip().split()[0]
+ except Exception:
+ pass
+
+ # Download new script
+ req = urllib.request.Request(update_url)
+ req.add_header("User-Agent", "JARVIS-Agent-macOS/3.0")
+ if _host_header:
+ req.add_header("Host", _host_header)
+ ctx = _make_ssl_ctx(ssl_verify)
+ with urllib.request.urlopen(req, timeout=30, context=ctx) as resp:
+ new_content = resp.read()
+
+ if expected_hash:
+ actual_hash = hashlib.sha256(new_content).hexdigest()
+ if actual_hash != expected_hash:
+ log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
+ return False
+
+ with open(script_path, "rb") as f:
+ current = f.read()
+ if new_content != current:
+ log(f"Update verified — replacing {script_path} and restarting...")
+ 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:
+ log(f"Self-update check failed: {e}")
+ return False
+
+# ── 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))
+ update_interval = int(cfg.get("update_check_hours", 24)) * 3600
+
+ # Always re-register on startup to refresh capabilities, version, IP
+ api_key = state.get("api_key", "")
+ registered_key = register(cfg, state)
+ if registered_key:
+ api_key = registered_key
+ elif not api_key:
+ while 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)
+
+ headers = {"X-Agent-Key": api_key}
+ last_metrics = 0
+ last_update_chk = 0
+
+ log(f"Agent v{AGENT_VERSION} (macOS) running. Polling {jarvis_url} every {heartbeat_every}s.")
+
+ while True:
+ tick_start = time.time()
+ now = tick_start
+ 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)
+ 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
+ if now - last_update_chk >= update_interval:
+ last_update_chk = now
+ self_update(cfg)
+
+ # Push metrics
+ 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)
+ last_metrics = now
+
+ except Exception as e:
+ log(f"[ERROR] Loop error: {e}")
+
+ time.sleep(heartbeat_every)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/agent/jarvis-agent-windows.py b/agent/jarvis-agent-windows.py
new file mode 100644
index 0000000..d66655c
--- /dev/null
+++ b/agent/jarvis-agent-windows.py
@@ -0,0 +1,534 @@
+#!/usr/bin/env python3
+"""
+JARVIS Agent for Windows — system monitor that reports metrics to JARVIS HUD.
+Install via PowerShell (as Admin): irm 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 hashlib
+from datetime import datetime
+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 = "3.0"
+
+# ── Logging ────────────────────────────────────────────────────────────────────
+
+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") 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/3.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/3.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)}
+
+# ── PowerShell helper ──────────────────────────────────────────────────────────
+
+def _ps(script: str, timeout: int = 8) -> str:
+ try:
+ r = subprocess.run(
+ ["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
+ capture_output=True, text=True, timeout=timeout
+ )
+ return r.stdout.strip()
+ except Exception:
+ return ""
+
+# ── Metrics ────────────────────────────────────────────────────────────────────
+
+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 get_cpu_percent() -> float:
+ 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", "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"]
+ import shutil
+ if Path(r"C:\Program Files\Docker\Docker\Docker Desktop.exe").exists():
+ caps.append("docker")
+ # Screenshot via PIL or PowerShell .NET (always available on Windows)
+ caps.append("screenshot")
+ caps.append("sysinfo")
+ 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",
+ "os_version": platform.version(),
+ "timestamp": datetime.utcnow().isoformat() + "Z",
+ }
+
+# ── Registration ───────────────────────────────────────────────────────────────
+
+def register(cfg: dict, state: dict) -> str:
+ hostname = cfg.get("hostname", socket.gethostname().lower())
+ agent_type = cfg.get("agent_type", "windows")
+ ip = get_local_ip()
+ capabilities = detect_capabilities(cfg)
+ agent_id = cfg.get("agent_id", f"{hostname}_windows")
+ ssl_verify = bool(cfg.get("ssl_verify", True))
+
+ log(f"Registering as '{agent_id}' ({agent_type}) 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"Registered. agent_id={state['agent_id']}")
+ return api_key
+
+# ── Screenshot ─────────────────────────────────────────────────────────────────
+
+def _take_screenshot(cmd_data: dict) -> dict:
+ import base64, tempfile
+ tmp = str(INSTALL_DIR / "screenshot_tmp.png")
+ method = "unknown"
+
+ # Try PIL/Pillow first
+ try:
+ from PIL import ImageGrab
+ img = ImageGrab.grab(all_screens=True)
+ img.save(tmp, "PNG")
+ method = "pil"
+ except ImportError:
+ pass
+ except Exception:
+ pass
+
+ # Fallback: PowerShell .NET screenshot (no extra packages needed)
+ if method == "unknown":
+ ps_script = (
+ "Add-Type -AssemblyName System.Windows.Forms,System.Drawing; "
+ "$s=[System.Windows.Forms.SystemInformation]::VirtualScreen; "
+ "$bmp=New-Object System.Drawing.Bitmap($s.Width,$s.Height); "
+ "$g=[System.Drawing.Graphics]::FromImage($bmp); "
+ "$g.CopyFromScreen($s.Location,[System.Drawing.Point]::Empty,$s.Size); "
+ f"$bmp.Save('{tmp}'); $g.Dispose(); $bmp.Dispose()"
+ )
+ try:
+ r = subprocess.run(
+ ["powershell", "-NoProfile", "-NonInteractive", "-Command", ps_script],
+ capture_output=True, timeout=15
+ )
+ if r.returncode == 0 and os.path.exists(tmp):
+ method = "powershell"
+ except Exception:
+ pass
+
+ if method == "unknown" or not os.path.exists(tmp):
+ return _sysinfo_snapshot()
+
+ try:
+ with open(tmp, "rb") as f:
+ raw = f.read()
+ b64 = base64.b64encode(raw).decode()
+ try:
+ os.unlink(tmp)
+ except Exception:
+ pass
+ return {
+ "success": True,
+ "method": method,
+ "image_b64": b64,
+ "image_mime": "image/png",
+ "file_size": len(raw),
+ "hostname": socket.gethostname(),
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e), "method": method}
+
+# ── Sysinfo ────────────────────────────────────────────────────────────────────
+
+def _sysinfo_snapshot() -> dict:
+ data = {"success": True, "hostname": socket.gethostname(),
+ "snapshot_type": "text", "screenshot_available": False, "platform": "Windows"}
+ try:
+ mem = get_memory()
+ data["mem_total_mb"] = int(mem.get("total_mb", 0))
+ data["mem_used_mb"] = int(mem.get("used_mb", 0))
+ data["mem_avail_mb"] = int(mem.get("free_mb", 0))
+ except Exception:
+ pass
+ try:
+ data["cpu_percent"] = get_cpu_percent()
+ except Exception:
+ pass
+ try:
+ data["disk"] = get_disk()
+ except Exception:
+ pass
+ try:
+ ut = get_uptime()
+ data["uptime"] = ut.get("human", "")
+ except Exception:
+ pass
+ try:
+ out = _ps("Get-Process | Sort-Object CPU -Descending | Select-Object -First 8 Name,CPU,WorkingSet | ConvertTo-Json")
+ data["top_procs"] = json.loads(out) if out else []
+ except Exception:
+ pass
+ try:
+ out = _ps("netstat -an | findstr LISTENING | head -20")
+ data["listening_ports"] = out[:800]
+ except Exception:
+ pass
+ return data
+
+# ── Self-update ────────────────────────────────────────────────────────────────
+
+def self_update(cfg: dict) -> bool:
+ jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
+ default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.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__)
+ ssl_verify = bool(cfg.get("ssl_verify", True))
+ try:
+ # Download expected hash
+ hash_url = update_url + ".sha256"
+ req_hash = urllib.request.Request(hash_url)
+ req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
+ if _host_header:
+ req_hash.add_header("Host", _host_header)
+ expected_hash = None
+ try:
+ ctx = _make_ssl_ctx(ssl_verify)
+ with urllib.request.urlopen(req_hash, timeout=10, context=ctx) as resp:
+ expected_hash = resp.read().decode().strip().split()[0]
+ except Exception:
+ pass
+
+ # Download new script
+ req = urllib.request.Request(update_url)
+ req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
+ if _host_header:
+ req.add_header("Host", _host_header)
+ ctx = _make_ssl_ctx(ssl_verify)
+ with urllib.request.urlopen(req, timeout=30, context=ctx) as resp:
+ new_content = resp.read()
+
+ # Verify hash
+ if expected_hash:
+ actual_hash = hashlib.sha256(new_content).hexdigest()
+ if actual_hash != expected_hash:
+ log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
+ return False
+
+ with open(script_path, "rb") as f:
+ current = f.read()
+ if new_content != current:
+ log(f"Update verified — replacing {script_path} and restarting...")
+ 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:
+ log(f"Self-update check failed: {e}")
+ return False
+
+# ── 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")
+ updated = self_update(cfg)
+ return {"success": True, "updated": updated}
+
+ elif cmd_type == "shell":
+ # Guard reads LOCAL config — never trust server-supplied flags
+ _cfg = load_config()
+ if not _cfg.get("allow_shell_commands", False):
+ return {"success": False, "error": "Shell commands not enabled in agent config"}
+ cmd_str = cmd_data.get("command", "")
+ r = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd_str],
+ capture_output=True, text=True, timeout=30)
+ return {"success": True, "stdout": r.stdout[:2000], "stderr": r.stderr[:500]}
+
+ elif cmd_type == "restart_service":
+ svc = cmd_data.get("service", "")
+ if not svc:
+ return {"success": False, "error": "No service specified"}
+ out = _ps(f"Restart-Service -Name '{svc}' -Force -ErrorAction Stop; 'ok'")
+ return {"success": "ok" in out.lower(), "output": out}
+
+ elif cmd_type == "get_logs":
+ svc = cmd_data.get("service", "")
+ lines = min(int(cmd_data.get("lines", 50)), 200)
+ if not svc:
+ return {"success": False, "error": "No service specified"}
+ out = _ps(f"Get-EventLog -LogName Application -Source '{svc}' -Newest {lines} -ErrorAction SilentlyContinue | Format-List TimeGenerated,EntryType,Message | Out-String")
+ return {"success": True, "output": out[:3000]}
+
+ elif cmd_type == "screenshot":
+ return _take_screenshot(cmd_data)
+
+ elif cmd_type == "sysinfo":
+ return _sysinfo_snapshot()
+
+ 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))
+ update_interval = int(cfg.get("update_check_hours", 24)) * 3600
+
+ # Always re-register on startup to refresh capabilities, version, IP
+ api_key = state.get("api_key", "")
+ registered_key = register(cfg, state)
+ if registered_key:
+ api_key = registered_key
+ elif not api_key:
+ while 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)
+
+ headers = {"X-Agent-Key": api_key}
+ last_metrics = 0
+ last_update_chk = 0
+
+ log(f"Agent v{AGENT_VERSION} (Windows) running. Polling {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)
+
+ # Self-update check
+ if now - last_update_chk >= update_interval:
+ last_update_chk = now
+ self_update(cfg)
+
+ # Push metrics
+ 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)
+ last_metrics = now
+
+ except Exception as e:
+ log(f"[ERROR] Loop error: {e}")
+
+ time.sleep(heartbeat_every)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/agent/jarvis-agent.py b/agent/jarvis-agent.py
index 8dcfe1e..4494c7a 100755
--- a/agent/jarvis-agent.py
+++ b/agent/jarvis-agent.py
@@ -23,7 +23,7 @@ from pathlib import Path
CONFIG_PATH = "/etc/jarvis-agent/config.json"
STATE_PATH = "/var/lib/jarvis-agent/state.json"
-AGENT_VERSION = "3.0" # Phase 4: screenshot + sysinfo commands
+AGENT_VERSION = "3.1"
# ── Config helpers ────────────────────────────────────────────────────────────
@@ -119,12 +119,6 @@ def detect_capabilities(cfg: dict) -> list:
# Check for Home Assistant
if os.path.exists("/etc/homeassistant") or os.path.exists("/config/configuration.yaml"):
caps.append("homeassistant")
- # Phase 4: screenshot capability
- import shutil as _shutil
- if (_shutil.which("scrot") or _shutil.which("import") or
- _shutil.which("fbcat") or _shutil.which("convert")):
- caps.append("screenshot")
- caps.append("sysinfo")
return caps
def register(cfg: dict, state: dict) -> str:
@@ -304,198 +298,6 @@ def collect_proxmox_metrics(cfg: dict) -> dict | None:
except Exception as e:
return {"error": str(e)}
-# ── Screenshot / Vision helpers ───────────────────────────────────────────────
-
-def _take_screenshot(cmd_data: dict) -> dict:
- """
- Attempts to capture a screenshot using available tools.
- For headless servers, falls back to a rich text system snapshot.
- Returns base64-encoded PNG and metadata.
- """
- import base64, tempfile, shutil
-
- tmp = tempfile.mktemp(suffix=".png")
- width = height = 0
- method = "unknown"
-
- # 1. Try scrot (X11 desktop)
- if shutil.which("scrot") and os.environ.get("DISPLAY"):
- try:
- r = subprocess.run(["scrot", "-z", tmp], capture_output=True, timeout=10)
- if r.returncode == 0 and os.path.exists(tmp):
- method = "scrot"
- except Exception:
- pass
-
- # 2. Try import (ImageMagick X11)
- if method == "unknown" and shutil.which("import") and os.environ.get("DISPLAY"):
- try:
- r = subprocess.run(["import", "-window", "root", tmp], capture_output=True, timeout=10)
- if r.returncode == 0 and os.path.exists(tmp):
- method = "import"
- except Exception:
- pass
-
- # 3. Try fbcat (Linux framebuffer — headless VMs with framebuffer)
- if method == "unknown" and shutil.which("fbcat") and os.path.exists("/dev/fb0"):
- try:
- ppm = tempfile.mktemp(suffix=".ppm")
- r = subprocess.run(["fbcat", "-s", "/dev/fb0"], stdout=open(ppm, "wb"),
- stderr=subprocess.PIPE, timeout=10)
- if r.returncode == 0 and shutil.which("convert"):
- subprocess.run(["convert", ppm, tmp], capture_output=True, timeout=10)
- os.unlink(ppm)
- if os.path.exists(tmp):
- method = "framebuffer"
- except Exception:
- pass
-
- # 4. Headless fallback: build a PNG system dashboard from text stats
- if method == "unknown":
- try:
- result = _render_sysinfo_png(tmp)
- if result:
- method = "sysinfo_render"
- except Exception:
- pass
-
- if method == "unknown" or not os.path.exists(tmp):
- # Last resort: return text snapshot only
- snap = _sysinfo_snapshot()
- snap["screenshot_available"] = False
- snap["method"] = "text_only"
- return snap
-
- # Read image
- try:
- with open(tmp, "rb") as f:
- raw = f.read()
- b64 = base64.b64encode(raw).decode()
- fsize = len(raw)
- os.unlink(tmp)
-
- # Try to get dimensions via file command
- try:
- r = subprocess.run(["identify", "-format", "%wx%h", tmp],
- capture_output=True, text=True, timeout=5)
- if "x" in r.stdout:
- w, h = r.stdout.strip().split("x", 1)
- width, height = int(w), int(h)
- except Exception:
- pass
-
- return {
- "success": True,
- "method": method,
- "image_b64": b64,
- "image_mime": "image/png",
- "file_size": fsize,
- "width": width,
- "height": height,
- "hostname": socket.gethostname(),
- }
- except Exception as e:
- return {"success": False, "error": str(e), "method": method}
-
-
-def _render_sysinfo_png(out_path: str) -> bool:
- """Render a system info text snapshot as a PNG using ansi2image or ImageMagick."""
- import shutil
- snap = _build_sysinfo_text()
- # Try convert (ImageMagick) to render text → PNG
- if shutil.which("convert"):
- try:
- r = subprocess.run([
- "convert",
- "-size", "900x600", "xc:#0a0f14",
- "-font", "Courier-New",
- "-pointsize", "13",
- "-fill", "#00d4ff",
- "-annotate", "+20+30", snap[:3000],
- out_path,
- ], capture_output=True, timeout=15)
- return r.returncode == 0 and os.path.exists(out_path)
- except Exception:
- pass
- return False
-
-
-def _build_sysinfo_text() -> str:
- """Build a rich text system snapshot for headless machines."""
- lines = [f"JARVIS FIELD STATION — {socket.gethostname()}",
- f"Timestamp: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}",
- "─" * 60]
- try:
- # CPU / mem / disk
- with open("/proc/loadavg") as f:
- load = f.read().split()[:3]
- lines.append(f"Load avg: {' '.join(load)}")
- except Exception:
- pass
- try:
- with open("/proc/meminfo") as f:
- minfo = {l.split(":")[0].strip(): int(l.split()[1]) for l in f if ":" in l}
- total = minfo.get("MemTotal", 0)
- avail = minfo.get("MemAvailable", 0)
- used = total - avail
- lines.append(f"Memory: {used//1024}MB used / {total//1024}MB total")
- except Exception:
- pass
- try:
- r = subprocess.run(["df", "-h", "/"], capture_output=True, text=True, timeout=5)
- lines.append("Disk:\n" + r.stdout.strip())
- except Exception:
- pass
- try:
- r = subprocess.run(["ps", "aux", "--sort=-%cpu"], capture_output=True, text=True, timeout=5)
- lines.append("Top processes:\n" + "\n".join(r.stdout.splitlines()[1:8]))
- except Exception:
- pass
- try:
- r = subprocess.run(["ss", "-tlnp"], capture_output=True, text=True, timeout=5)
- lines.append("Listening ports:\n" + r.stdout.strip()[:500])
- except Exception:
- pass
- return "\n".join(lines)
-
-
-def _sysinfo_snapshot() -> dict:
- """Return structured system snapshot (no image) for text-based analysis."""
- data = {"success": True, "hostname": socket.gethostname(),
- "snapshot_type": "text", "screenshot_available": False}
- try:
- with open("/proc/loadavg") as f:
- parts = f.read().split()
- data["load_1m"], data["load_5m"], data["load_15m"] = parts[0], parts[1], parts[2]
- except Exception:
- pass
- try:
- with open("/proc/meminfo") as f:
- m = {l.split(":")[0].strip(): int(l.split()[1])
- for l in f if ":" in l and len(l.split()) >= 2}
- data["mem_total_mb"] = m.get("MemTotal", 0) // 1024
- data["mem_avail_mb"] = m.get("MemAvailable", 0) // 1024
- data["mem_used_mb"] = data["mem_total_mb"] - data["mem_avail_mb"]
- except Exception:
- pass
- try:
- r = subprocess.run(["df", "-h", "/"], capture_output=True, text=True, timeout=5)
- data["disk"] = r.stdout.splitlines()[1] if r.stdout else ""
- except Exception:
- pass
- try:
- r = subprocess.run(["ps", "aux", "--sort=-%cpu"], capture_output=True, text=True, timeout=5)
- data["top_procs"] = r.stdout.splitlines()[1:8]
- except Exception:
- pass
- try:
- r = subprocess.run(["ss", "-tlnp"], capture_output=True, text=True, timeout=5)
- data["listening_ports"] = r.stdout.strip()[:800]
- except Exception:
- pass
- return data
-
-
# ── Command execution ─────────────────────────────────────────────────────────
def execute_command(cmd: dict) -> dict:
@@ -525,25 +327,17 @@ def execute_command(cmd: dict) -> dict:
return {"success": r.returncode == 0, "output": r.stdout}
elif cmd_type == "update":
- _cfg = load_config()
- updated = self_update(_cfg)
+ updated = self_update(cfg)
return {"success": True, "updated": updated}
elif cmd_type == "shell":
- # Guard reads LOCAL config, not the server-supplied payload
- _cfg = load_config()
- if not _cfg.get("allow_shell_commands", False):
- return {"success": False, "error": "Shell commands not enabled in agent config"}
+ # 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]}
- elif cmd_type == "screenshot":
- return _take_screenshot(cmd_data)
-
- elif cmd_type == "sysinfo":
- return _sysinfo_snapshot()
-
else:
return {"success": False, "error": f"Unknown command type: {cmd_type}"}
@@ -565,18 +359,15 @@ def main():
poll_interval = int(cfg.get("poll_interval", 30))
heartbeat_every = int(cfg.get("heartbeat_every", 10))
- # Always re-register on startup to refresh capabilities, version, and IP.
- # Server does an UPDATE when agent_id already exists, so api_key is preserved.
+ # Register if no API key yet
api_key = state.get("api_key", "")
- registered_key = register(cfg, state)
- if registered_key:
- api_key = registered_key
- elif not api_key:
- while 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)
+ 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
@@ -633,9 +424,7 @@ def main():
# ── Self-update ────────────────────────────────────────────────────────────────
def self_update(cfg: dict) -> bool:
- """Check JARVIS server for a newer version of this script.
- Verifies SHA-256 hash from .sha256 before replacing."""
- import hashlib
+ """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)
@@ -643,37 +432,14 @@ def self_update(cfg: dict) -> bool:
return False
script_path = os.path.abspath(__file__)
try:
- # Download expected hash first
- hash_url = update_url + ".sha256"
- req_hash = urllib.request.Request(hash_url)
- req_hash.add_header("User-Agent", "JARVIS-Agent/1.0")
- if _host_header:
- req_hash.add_header("Host", _host_header)
- try:
- with urllib.request.urlopen(req_hash, timeout=10) as resp:
- expected_hash = resp.read().decode().strip().split()[0]
- except Exception:
- expected_hash = None
-
- # Download new script
req = urllib.request.Request(update_url)
req.add_header("User-Agent", "JARVIS-Agent/1.0")
- if _host_header:
- req.add_header("Host", _host_header)
with urllib.request.urlopen(req, timeout=30) as resp:
new_content = resp.read()
-
- # Verify hash if available — abort if mismatch
- if expected_hash:
- actual_hash = hashlib.sha256(new_content).hexdigest()
- if actual_hash != expected_hash:
- print(f"[JARVIS] Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting", flush=True)
- return False
-
with open(script_path, "rb") as f:
current = f.read()
if new_content != current:
- print(f"[JARVIS] Update verified — replacing {script_path} and restarting...", flush=True)
+ 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)
diff --git a/public_html/agent/install-mac.sh b/public_html/agent/install-mac.sh
index 0f8777b..9f5fda0 100644
--- a/public_html/agent/install-mac.sh
+++ b/public_html/agent/install-mac.sh
@@ -1,13 +1,13 @@
#!/bin/bash
# JARVIS Agent Installer — macOS
# Usage: bash install-mac.sh --jarvis-url https://jarvis.orbishosting.com --key YOUR_KEY
+# Or one-liner: bash <(curl -sSL https://jarvis.orbishosting.com/agent/install-mac.sh) --key YOUR_KEY
set -e
-JARVIS_URL=""
+JARVIS_URL="https://jarvis.orbishosting.com"
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"
@@ -19,59 +19,67 @@ while [[ $# -gt 0 ]]; do
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%/}"
+echo ""
+echo " ===================================="
+echo " JARVIS Agent Installer v3.0 "
+echo " ===================================="
+echo ""
+
# Check for Python3
-PYTHON3=$(command -v python3 2>/dev/null || command -v /usr/bin/python3 2>/dev/null || "")
+PYTHON3=$(command -v 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))"
+echo "Using Python: $PYTHON3 ($($PYTHON3 --version 2>&1))"
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"
+# Download macOS-native agent script
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || echo "")"
+if [[ -f "$SCRIPT_DIR/jarvis-agent-mac.py" ]]; then
+ cp "$SCRIPT_DIR/jarvis-agent-mac.py" "$INSTALL_DIR/jarvis-agent.py"
+ echo "Copied agent from local directory."
else
- echo "Downloading agent..."
- curl -sSL "https://raw.githubusercontent.com/myronblair/jarvis/master/agent/jarvis-agent.py" \
- -o "$INSTALL_DIR/jarvis-agent.py"
+ echo "Downloading macOS agent..."
+ curl -sSL "$JARVIS_URL/agent/jarvis-agent-mac.py" -o "$INSTALL_DIR/jarvis-agent.py"
+ echo "Downloaded."
fi
chmod +x "$INSTALL_DIR/jarvis-agent.py"
HOSTNAME=$(hostname -f 2>/dev/null || hostname)
+AGENT_ID="${HOSTNAME}_mac"
-# Write config
-if [[ ! -f "$CONFIG_DIR/config.json" ]]; then
-cat > "$CONFIG_DIR/config.json" << JSONEOF
+# Write config (preserve existing)
+if [[ ! -f "$INSTALL_DIR/config.json" ]]; then
+cat > "$INSTALL_DIR/config.json" << JSONEOF
{
"jarvis_url": "$JARVIS_URL",
"registration_key": "$REG_KEY",
"hostname": "$HOSTNAME",
- "agent_type": "linux",
+ "agent_id": "$AGENT_ID",
+ "agent_type": "macos",
"poll_interval": 30,
"heartbeat_every": 10,
- "watch_services": []
+ "update_check_hours": 24,
+ "watch_services": [],
+ "allow_shell_commands": false
}
JSONEOF
- chmod 600 "$CONFIG_DIR/config.json"
+ chmod 600 "$INSTALL_DIR/config.json"
+ echo "Config written to $INSTALL_DIR/config.json"
+else
+ echo "Config already exists — preserving $INSTALL_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
@@ -89,9 +97,9 @@ cat > "$PLIST_PATH" << PLISTEOF
EnvironmentVariables
JARVIS_CONFIG
- $CONFIG_DIR/config.json
+ $INSTALL_DIR/config.json
JARVIS_STATE
- $STATE_PATH
+ $INSTALL_DIR/state.json
RunAtLoad
@@ -105,18 +113,23 @@ cat > "$PLIST_PATH" << PLISTEOF
PLISTEOF
-# Load the service
+# Load/reload the service
launchctl unload "$PLIST_PATH" 2>/dev/null || true
launchctl load "$PLIST_PATH"
sleep 2
-if launchctl list | grep -q "$SERVICE_LABEL"; then
+if launchctl list 2>/dev/null | 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"
+ echo " JARVIS Agent installed and running."
+ echo " Machine : $HOSTNAME ($AGENT_ID)"
+ echo " JARVIS : $JARVIS_URL"
+ echo " Logs : tail -f $INSTALL_DIR/jarvis-agent.log"
+ echo " Config : $INSTALL_DIR/config.json"
+ echo " Stop : launchctl unload $PLIST_PATH"
+ echo " Update : curl -sSL $JARVIS_URL/agent/install-mac.sh | bash -s -- --key $REG_KEY"
else
- echo "⚠ Agent installed but not running. Check logs:"
+ echo ""
+ echo " Agent installed but not detected as running. Check logs:"
echo " tail -f $INSTALL_DIR/jarvis-agent.log"
fi
+echo ""
diff --git a/public_html/agent/jarvis-agent-mac.py b/public_html/agent/jarvis-agent-mac.py
new file mode 100644
index 0000000..6bd487a
--- /dev/null
+++ b/public_html/agent/jarvis-agent-mac.py
@@ -0,0 +1,576 @@
+#!/usr/bin/env python3
+"""
+JARVIS Agent for macOS — system monitor that reports metrics to JARVIS HUD.
+Install: bash <(curl -sSL https://jarvis.orbishosting.com/agent/install-mac.sh) --key YOUR_KEY
+Config: ~/.jarvis-agent/config.json (or $JARVIS_CONFIG)
+Logs: ~/.jarvis-agent/jarvis-agent.log (or journalctl via launchd)
+"""
+
+import json
+import os
+import platform
+import re
+import socket
+import subprocess
+import sys
+import time
+import urllib.request
+import urllib.error
+import hashlib
+from datetime import datetime
+from pathlib import Path
+
+AGENT_VERSION = "3.0"
+
+# Config/state paths — env vars let the launchd plist override them
+_default_dir = Path.home() / ".jarvis-agent"
+CONFIG_PATH = Path(os.environ.get("JARVIS_CONFIG", str(_default_dir / "config.json")))
+STATE_PATH = Path(os.environ.get("JARVIS_STATE", str(_default_dir / "state.json")))
+LOG_PATH = _default_dir / "jarvis-agent.log"
+
+# ── Logging ────────────────────────────────────────────────────────────────────
+
+def log(msg: str):
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ line = f"[{ts}] {msg}"
+ print(line, flush=True)
+ try:
+ LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
+ with open(LOG_PATH, "a") 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) as f:
+ return json.load(f)
+
+def load_state() -> dict:
+ if STATE_PATH.exists():
+ with open(STATE_PATH) as f:
+ return json.load(f)
+ return {}
+
+def save_state(state: dict):
+ STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
+ with open(STATE_PATH, "w") 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-macOS/3.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-macOS/3.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 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_times = None
+
+def get_cpu_percent() -> float:
+ """Delta-based CPU % using top (two samples)."""
+ global _last_cpu_times
+ try:
+ # top -l 2 gives two snapshots; second line has the real delta
+ r = subprocess.run(
+ ["top", "-l", "2", "-s", "0", "-n", "0"],
+ capture_output=True, text=True, timeout=12
+ )
+ idle = None
+ for line in r.stdout.splitlines():
+ if "CPU usage:" in line:
+ m = re.search(r"([\d.]+)%\s+idle", line)
+ if m:
+ idle = float(m.group(1))
+ if idle is not None:
+ return round(100.0 - idle, 1)
+ except Exception:
+ pass
+ return 0.0
+
+def get_memory() -> dict:
+ try:
+ # Total physical memory
+ r = subprocess.run(["sysctl", "-n", "hw.memsize"], capture_output=True, text=True, timeout=3)
+ total_bytes = int(r.stdout.strip())
+
+ # vm_stat for page counts; default page size on Apple Silicon and Intel = 4096
+ page_size = 4096
+ try:
+ ps_r = subprocess.run(["sysctl", "-n", "hw.pagesize"], capture_output=True, text=True, timeout=3)
+ page_size = int(ps_r.stdout.strip())
+ except Exception:
+ pass
+
+ vm_r = subprocess.run(["vm_stat"], capture_output=True, text=True, timeout=5)
+ pages = {}
+ for line in vm_r.stdout.splitlines():
+ m = re.match(r"Pages\s+(.+?):\s+([\d]+)", line)
+ if m:
+ pages[m.group(1).strip().lower()] = int(m.group(2))
+
+ free_pages = pages.get("free", 0) + pages.get("speculative", 0)
+ # "available" = free + inactive (can be reclaimed)
+ avail_pages = free_pages + pages.get("inactive", 0)
+ used_pages = (total_bytes // page_size) - avail_pages
+
+ total_mb = round(total_bytes / (1024 * 1024), 1)
+ used_mb = round(used_pages * page_size / (1024 * 1024), 1)
+ avail_mb = round(avail_pages * page_size / (1024 * 1024), 1)
+ used_mb = max(0, min(used_mb, total_mb))
+
+ return {
+ "total_mb": total_mb,
+ "used_mb": used_mb,
+ "free_mb": avail_mb,
+ "percent": round(used_mb / total_mb * 100, 1) if total_mb else 0,
+ }
+ except Exception:
+ return {}
+
+def get_disk() -> list:
+ disks = []
+ try:
+ r = subprocess.run(["df", "-H", "-l"], capture_output=True, text=True, timeout=5)
+ for line in r.stdout.splitlines()[1:]:
+ parts = line.split()
+ if len(parts) >= 6:
+ mount = parts[5]
+ if not any(mount.startswith(x) for x in ["/dev", "/private/var/vm", "/System/Volumes/VM"]):
+ disks.append({
+ "mount": mount,
+ "size": parts[1],
+ "used": parts[2],
+ "avail": parts[3],
+ "percent": parts[4].rstrip("%"),
+ })
+ except Exception:
+ pass
+ return disks
+
+def get_uptime() -> dict:
+ try:
+ r = subprocess.run(["sysctl", "-n", "kern.boottime"], capture_output=True, text=True, timeout=3)
+ m = re.search(r"sec\s*=\s*(\d+)", r.stdout)
+ if m:
+ boot_ts = int(m.group(1))
+ secs = time.time() - boot_ts
+ 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:
+ pass
+ return {}
+
+def get_load() -> list:
+ try:
+ r = subprocess.run(["sysctl", "-n", "vm.loadavg"], capture_output=True, text=True, timeout=3)
+ # format: "{ 0.12 0.34 0.56 }"
+ nums = re.findall(r"[\d.]+", r.stdout)
+ if len(nums) >= 3:
+ return [float(nums[0]), float(nums[1]), float(nums[2])]
+ except Exception:
+ pass
+ return [0, 0, 0]
+
+def get_services(cfg: dict) -> list:
+ """Check macOS LaunchDaemon/LaunchAgent services via launchctl."""
+ watch = cfg.get("watch_services", [])
+ if not watch:
+ return []
+ statuses = []
+ try:
+ r = subprocess.run(["launchctl", "list"], capture_output=True, text=True, timeout=5)
+ running = set()
+ for line in r.stdout.splitlines():
+ parts = line.split()
+ if len(parts) >= 3:
+ running.add(parts[2])
+ except Exception:
+ running = set()
+ for svc in watch:
+ status = "active" if any(svc in s for s in running) else "inactive"
+ statuses.append({"service": svc, "status": status})
+ return statuses
+
+def detect_capabilities(cfg: dict) -> list:
+ import shutil
+ caps = ["metrics", "commands"]
+ if shutil.which("docker"):
+ caps.append("docker")
+ if shutil.which("ollama"):
+ caps.append("ollama")
+ # screencapture is always available on macOS
+ caps.append("screenshot")
+ caps.append("sysinfo")
+ 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": get_load(),
+ "services": get_services(cfg),
+ "platform": "macOS",
+ "os_version": platform.mac_ver()[0],
+ "timestamp": datetime.utcnow().isoformat() + "Z",
+ }
+
+# ── Registration ───────────────────────────────────────────────────────────────
+
+def register(cfg: dict, state: dict) -> str:
+ hostname = cfg.get("hostname", socket.gethostname())
+ agent_type = cfg.get("agent_type", "macos")
+ ip = get_local_ip()
+ capabilities = detect_capabilities(cfg)
+ agent_id = cfg.get("agent_id", f"{hostname}_mac")
+ ssl_verify = bool(cfg.get("ssl_verify", True))
+
+ log(f"Registering as '{agent_id}' ({agent_type}) 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"Registered. agent_id={state['agent_id']}")
+ return api_key
+
+# ── Screenshot ─────────────────────────────────────────────────────────────────
+
+def _take_screenshot(cmd_data: dict) -> dict:
+ import base64, tempfile, shutil
+ tmp = tempfile.mktemp(suffix=".png")
+ method = "unknown"
+
+ # screencapture -x = no sound, always available on macOS
+ try:
+ r = subprocess.run(["screencapture", "-x", "-t", "png", tmp],
+ capture_output=True, timeout=15)
+ if r.returncode == 0 and os.path.exists(tmp):
+ method = "screencapture"
+ except Exception:
+ pass
+
+ # Fallback: PIL
+ if method == "unknown":
+ try:
+ from PIL import ImageGrab
+ img = ImageGrab.grab()
+ img.save(tmp, "PNG")
+ method = "pil"
+ except Exception:
+ pass
+
+ if method == "unknown" or not os.path.exists(tmp):
+ snap = _sysinfo_snapshot()
+ snap["screenshot_available"] = False
+ snap["method"] = "text_only"
+ return snap
+
+ try:
+ with open(tmp, "rb") as f:
+ raw = f.read()
+ b64 = base64.b64encode(raw).decode()
+ try:
+ os.unlink(tmp)
+ except Exception:
+ pass
+ return {
+ "success": True,
+ "method": method,
+ "image_b64": b64,
+ "image_mime": "image/png",
+ "file_size": len(raw),
+ "hostname": socket.gethostname(),
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e), "method": method}
+
+# ── Sysinfo ────────────────────────────────────────────────────────────────────
+
+def _sysinfo_snapshot() -> dict:
+ data = {"success": True, "hostname": socket.gethostname(),
+ "snapshot_type": "text", "screenshot_available": False, "platform": "macOS"}
+ try:
+ mem = get_memory()
+ data["mem_total_mb"] = int(mem.get("total_mb", 0))
+ data["mem_used_mb"] = int(mem.get("used_mb", 0))
+ data["mem_avail_mb"] = int(mem.get("free_mb", 0))
+ except Exception:
+ pass
+ try:
+ load = get_load()
+ data["load_1m"], data["load_5m"], data["load_15m"] = load[0], load[1], load[2]
+ except Exception:
+ pass
+ try:
+ r = subprocess.run(["df", "-H", "/"], capture_output=True, text=True, timeout=5)
+ data["disk"] = r.stdout.splitlines()[1] if r.stdout else ""
+ except Exception:
+ pass
+ try:
+ r = subprocess.run(["ps", "aux", "-r"], capture_output=True, text=True, timeout=5)
+ data["top_procs"] = r.stdout.splitlines()[1:8]
+ except Exception:
+ pass
+ try:
+ r = subprocess.run(["netstat", "-an", "-p", "tcp"], capture_output=True, text=True, timeout=5)
+ listening = [l for l in r.stdout.splitlines() if "LISTEN" in l]
+ data["listening_ports"] = "\n".join(listening[:20])
+ except Exception:
+ pass
+ return data
+
+# ── Command execution ──────────────────────────────────────────────────────────
+
+def execute_command(cmd: 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", "-c", "3", "-W", "2000", host],
+ capture_output=True, text=True, timeout=15)
+ return {"success": r.returncode == 0, "output": r.stdout}
+
+ elif cmd_type == "update":
+ _cfg = load_config()
+ updated = self_update(_cfg)
+ return {"success": True, "updated": updated}
+
+ elif cmd_type == "shell":
+ _cfg = load_config()
+ if not _cfg.get("allow_shell_commands", False):
+ return {"success": False, "error": "Shell commands not enabled in agent config"}
+ 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]}
+
+ elif 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(["launchctl", "kickstart", "-k", f"system/{svc}"],
+ capture_output=True, text=True, timeout=15)
+ if r.returncode != 0:
+ r = subprocess.run(["brew", "services", "restart", svc],
+ capture_output=True, text=True, timeout=15)
+ 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)
+ r = subprocess.run(["log", "show", "--predicate", f'process == "{svc}"',
+ "--last", "1h", "--style", "compact"],
+ capture_output=True, text=True, timeout=15)
+ output = "\n".join(r.stdout.splitlines()[-lines:])
+ return {"success": True, "output": output}
+
+ elif cmd_type == "screenshot":
+ return _take_screenshot(cmd_data)
+
+ elif cmd_type == "sysinfo":
+ return _sysinfo_snapshot()
+
+ 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)}
+
+# ── Self-update ────────────────────────────────────────────────────────────────
+
+def self_update(cfg: dict) -> bool:
+ jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
+ default_update_url = f"{jarvis_url}/agent/jarvis-agent-mac.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__)
+ ssl_verify = bool(cfg.get("ssl_verify", True))
+ try:
+ # Download expected hash
+ hash_url = update_url + ".sha256"
+ req_hash = urllib.request.Request(hash_url)
+ req_hash.add_header("User-Agent", "JARVIS-Agent-macOS/3.0")
+ if _host_header:
+ req_hash.add_header("Host", _host_header)
+ expected_hash = None
+ try:
+ ctx = _make_ssl_ctx(ssl_verify)
+ with urllib.request.urlopen(req_hash, timeout=10, context=ctx) as resp:
+ expected_hash = resp.read().decode().strip().split()[0]
+ except Exception:
+ pass
+
+ # Download new script
+ req = urllib.request.Request(update_url)
+ req.add_header("User-Agent", "JARVIS-Agent-macOS/3.0")
+ if _host_header:
+ req.add_header("Host", _host_header)
+ ctx = _make_ssl_ctx(ssl_verify)
+ with urllib.request.urlopen(req, timeout=30, context=ctx) as resp:
+ new_content = resp.read()
+
+ if expected_hash:
+ actual_hash = hashlib.sha256(new_content).hexdigest()
+ if actual_hash != expected_hash:
+ log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
+ return False
+
+ with open(script_path, "rb") as f:
+ current = f.read()
+ if new_content != current:
+ log(f"Update verified — replacing {script_path} and restarting...")
+ 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:
+ log(f"Self-update check failed: {e}")
+ return False
+
+# ── 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))
+ update_interval = int(cfg.get("update_check_hours", 24)) * 3600
+
+ # Always re-register on startup to refresh capabilities, version, IP
+ api_key = state.get("api_key", "")
+ registered_key = register(cfg, state)
+ if registered_key:
+ api_key = registered_key
+ elif not api_key:
+ while 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)
+
+ headers = {"X-Agent-Key": api_key}
+ last_metrics = 0
+ last_update_chk = 0
+
+ log(f"Agent v{AGENT_VERSION} (macOS) running. Polling {jarvis_url} every {heartbeat_every}s.")
+
+ while True:
+ tick_start = time.time()
+ now = tick_start
+ 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)
+ 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
+ if now - last_update_chk >= update_interval:
+ last_update_chk = now
+ self_update(cfg)
+
+ # Push metrics
+ 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)
+ last_metrics = now
+
+ except Exception as e:
+ log(f"[ERROR] Loop error: {e}")
+
+ time.sleep(heartbeat_every)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/public_html/agent/jarvis-agent-mac.py.sha256 b/public_html/agent/jarvis-agent-mac.py.sha256
new file mode 100644
index 0000000..dc36921
--- /dev/null
+++ b/public_html/agent/jarvis-agent-mac.py.sha256
@@ -0,0 +1 @@
+d0aeaab1686f788c9d46ffeaac57a42e3e82b80c2d7fac2be443c05cf70c87be jarvis-agent-mac.py
diff --git a/public_html/agent/jarvis-agent-windows.py b/public_html/agent/jarvis-agent-windows.py
index efa3688..d66655c 100644
--- a/public_html/agent/jarvis-agent-windows.py
+++ b/public_html/agent/jarvis-agent-windows.py
@@ -1,9 +1,9 @@
#!/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
+Install via PowerShell (as Admin): irm 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
@@ -15,20 +15,18 @@ import sys
import time
import urllib.request
import urllib.error
-import uuid
-from datetime import datetime, timezone
+import hashlib
+from datetime import datetime
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"
+AGENT_VERSION = "3.0"
# ── Logging ────────────────────────────────────────────────────────────────────
-_log_file = None
-
def log(msg: str):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
line = f"[{ts}] {msg}"
@@ -45,7 +43,7 @@ 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:
+ with open(CONFIG_PATH, encoding="utf-8") as f:
return json.load(f)
def load_state() -> dict:
@@ -78,7 +76,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
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")
+ req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
if _host_header:
req.add_header("Host", _host_header)
for k, v in headers.items():
@@ -95,7 +93,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
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")
+ req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
if _host_header:
req.add_header("Host", _host_header)
for k, v in headers.items():
@@ -107,10 +105,9 @@ def api_get(url: str, headers: dict = {}, timeout: int = 10,
except Exception as e:
return {"error": str(e)}
-# ── Metrics ────────────────────────────────────────────────────────────────────
+# ── PowerShell helper ──────────────────────────────────────────────────────────
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],
@@ -120,6 +117,8 @@ def _ps(script: str, timeout: int = 8) -> str:
except Exception:
return ""
+# ── Metrics ────────────────────────────────────────────────────────────────────
+
def get_local_ip() -> str:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -130,10 +129,7 @@ def get_local_ip() -> str:
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)
@@ -196,7 +192,7 @@ def get_uptime() -> dict:
return {}
def get_services(cfg: dict) -> list:
- watch = cfg.get("watch_services", ["WinDefend", "wuauserv", "Spooler"])
+ watch = cfg.get("watch_services", ["WinDefend", "Spooler"])
statuses = []
for svc in watch:
try:
@@ -209,8 +205,12 @@ def get_services(cfg: dict) -> list:
def detect_capabilities(cfg: dict) -> list:
caps = ["metrics", "commands"]
+ import shutil
if Path(r"C:\Program Files\Docker\Docker\Docker Desktop.exe").exists():
caps.append("docker")
+ # Screenshot via PIL or PowerShell .NET (always available on Windows)
+ caps.append("screenshot")
+ caps.append("sysinfo")
return caps
def collect_metrics(cfg: dict) -> dict:
@@ -223,20 +223,21 @@ def collect_metrics(cfg: dict) -> dict:
"load": [0, 0, 0],
"services": get_services(cfg),
"platform": "Windows",
- "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
+ "os_version": platform.version(),
+ "timestamp": datetime.utcnow().isoformat() + "Z",
}
# ── Registration ───────────────────────────────────────────────────────────────
def register(cfg: dict, state: dict) -> str:
hostname = cfg.get("hostname", socket.gethostname().lower())
- agent_type = cfg.get("agent_type", "linux")
+ agent_type = cfg.get("agent_type", "windows")
ip = get_local_ip()
capabilities = detect_capabilities(cfg)
- agent_id = cfg.get("agent_id", f"{hostname}_{hostname[:8]}")
+ agent_id = cfg.get("agent_id", f"{hostname}_windows")
ssl_verify = bool(cfg.get("ssl_verify", True))
- log(f"[JARVIS] Registering as '{agent_id}' from {ip}...")
+ log(f"Registering as '{agent_id}' ({agent_type}) from {ip}...")
result = api_post(
f"{cfg['jarvis_url']}/api/agent/register",
@@ -255,9 +256,160 @@ def register(cfg: dict, state: dict) -> str:
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']}")
+ log(f"Registered. agent_id={state['agent_id']}")
return api_key
+# ── Screenshot ─────────────────────────────────────────────────────────────────
+
+def _take_screenshot(cmd_data: dict) -> dict:
+ import base64, tempfile
+ tmp = str(INSTALL_DIR / "screenshot_tmp.png")
+ method = "unknown"
+
+ # Try PIL/Pillow first
+ try:
+ from PIL import ImageGrab
+ img = ImageGrab.grab(all_screens=True)
+ img.save(tmp, "PNG")
+ method = "pil"
+ except ImportError:
+ pass
+ except Exception:
+ pass
+
+ # Fallback: PowerShell .NET screenshot (no extra packages needed)
+ if method == "unknown":
+ ps_script = (
+ "Add-Type -AssemblyName System.Windows.Forms,System.Drawing; "
+ "$s=[System.Windows.Forms.SystemInformation]::VirtualScreen; "
+ "$bmp=New-Object System.Drawing.Bitmap($s.Width,$s.Height); "
+ "$g=[System.Drawing.Graphics]::FromImage($bmp); "
+ "$g.CopyFromScreen($s.Location,[System.Drawing.Point]::Empty,$s.Size); "
+ f"$bmp.Save('{tmp}'); $g.Dispose(); $bmp.Dispose()"
+ )
+ try:
+ r = subprocess.run(
+ ["powershell", "-NoProfile", "-NonInteractive", "-Command", ps_script],
+ capture_output=True, timeout=15
+ )
+ if r.returncode == 0 and os.path.exists(tmp):
+ method = "powershell"
+ except Exception:
+ pass
+
+ if method == "unknown" or not os.path.exists(tmp):
+ return _sysinfo_snapshot()
+
+ try:
+ with open(tmp, "rb") as f:
+ raw = f.read()
+ b64 = base64.b64encode(raw).decode()
+ try:
+ os.unlink(tmp)
+ except Exception:
+ pass
+ return {
+ "success": True,
+ "method": method,
+ "image_b64": b64,
+ "image_mime": "image/png",
+ "file_size": len(raw),
+ "hostname": socket.gethostname(),
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e), "method": method}
+
+# ── Sysinfo ────────────────────────────────────────────────────────────────────
+
+def _sysinfo_snapshot() -> dict:
+ data = {"success": True, "hostname": socket.gethostname(),
+ "snapshot_type": "text", "screenshot_available": False, "platform": "Windows"}
+ try:
+ mem = get_memory()
+ data["mem_total_mb"] = int(mem.get("total_mb", 0))
+ data["mem_used_mb"] = int(mem.get("used_mb", 0))
+ data["mem_avail_mb"] = int(mem.get("free_mb", 0))
+ except Exception:
+ pass
+ try:
+ data["cpu_percent"] = get_cpu_percent()
+ except Exception:
+ pass
+ try:
+ data["disk"] = get_disk()
+ except Exception:
+ pass
+ try:
+ ut = get_uptime()
+ data["uptime"] = ut.get("human", "")
+ except Exception:
+ pass
+ try:
+ out = _ps("Get-Process | Sort-Object CPU -Descending | Select-Object -First 8 Name,CPU,WorkingSet | ConvertTo-Json")
+ data["top_procs"] = json.loads(out) if out else []
+ except Exception:
+ pass
+ try:
+ out = _ps("netstat -an | findstr LISTENING | head -20")
+ data["listening_ports"] = out[:800]
+ except Exception:
+ pass
+ return data
+
+# ── Self-update ────────────────────────────────────────────────────────────────
+
+def self_update(cfg: dict) -> bool:
+ jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
+ default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.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__)
+ ssl_verify = bool(cfg.get("ssl_verify", True))
+ try:
+ # Download expected hash
+ hash_url = update_url + ".sha256"
+ req_hash = urllib.request.Request(hash_url)
+ req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
+ if _host_header:
+ req_hash.add_header("Host", _host_header)
+ expected_hash = None
+ try:
+ ctx = _make_ssl_ctx(ssl_verify)
+ with urllib.request.urlopen(req_hash, timeout=10, context=ctx) as resp:
+ expected_hash = resp.read().decode().strip().split()[0]
+ except Exception:
+ pass
+
+ # Download new script
+ req = urllib.request.Request(update_url)
+ req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
+ if _host_header:
+ req.add_header("Host", _host_header)
+ ctx = _make_ssl_ctx(ssl_verify)
+ with urllib.request.urlopen(req, timeout=30, context=ctx) as resp:
+ new_content = resp.read()
+
+ # Verify hash
+ if expected_hash:
+ actual_hash = hashlib.sha256(new_content).hexdigest()
+ if actual_hash != expected_hash:
+ log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
+ return False
+
+ with open(script_path, "rb") as f:
+ current = f.read()
+ if new_content != current:
+ log(f"Update verified — replacing {script_path} and restarting...")
+ 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:
+ log(f"Self-update check failed: {e}")
+ return False
+
# ── Command execution ──────────────────────────────────────────────────────────
def execute_command(cmd: dict, cfg: dict) -> dict:
@@ -271,16 +423,40 @@ def execute_command(cmd: dict, cfg: dict) -> dict:
elif cmd_type == "update":
log("[CMD] Self-update requested")
- return {"success": True, "message": "Windows agent self-update not implemented"}
+ updated = self_update(cfg)
+ return {"success": True, "updated": updated}
elif cmd_type == "shell":
- if not cmd_data.get("allowed", False):
- return {"success": False, "error": "Shell commands not enabled"}
+ # Guard reads LOCAL config — never trust server-supplied flags
+ _cfg = load_config()
+ if not _cfg.get("allow_shell_commands", False):
+ return {"success": False, "error": "Shell commands not enabled in agent config"}
cmd_str = cmd_data.get("command", "")
- r = subprocess.run(["powershell", "-NoProfile", "-Command", cmd_str],
+ r = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd_str],
capture_output=True, text=True, timeout=30)
return {"success": True, "stdout": r.stdout[:2000], "stderr": r.stderr[:500]}
+ elif cmd_type == "restart_service":
+ svc = cmd_data.get("service", "")
+ if not svc:
+ return {"success": False, "error": "No service specified"}
+ out = _ps(f"Restart-Service -Name '{svc}' -Force -ErrorAction Stop; 'ok'")
+ return {"success": "ok" in out.lower(), "output": out}
+
+ elif cmd_type == "get_logs":
+ svc = cmd_data.get("service", "")
+ lines = min(int(cmd_data.get("lines", 50)), 200)
+ if not svc:
+ return {"success": False, "error": "No service specified"}
+ out = _ps(f"Get-EventLog -LogName Application -Source '{svc}' -Newest {lines} -ErrorAction SilentlyContinue | Format-List TimeGenerated,EntryType,Message | Out-String")
+ return {"success": True, "output": out[:3000]}
+
+ elif cmd_type == "screenshot":
+ return _take_screenshot(cmd_data)
+
+ elif cmd_type == "sysinfo":
+ return _sysinfo_snapshot()
+
else:
return {"success": False, "error": f"Unknown command type: {cmd_type}"}
@@ -302,19 +478,25 @@ def main():
_host_header = cfg.get("host_header", "")
poll_interval = int(cfg.get("poll_interval", 30))
heartbeat_every = int(cfg.get("heartbeat_every", 10))
+ update_interval = int(cfg.get("update_check_hours", 24)) * 3600
+ # Always re-register on startup to refresh capabilities, version, IP
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
+ registered_key = register(cfg, state)
+ if registered_key:
+ api_key = registered_key
+ elif not api_key:
+ while 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)
- 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.")
+ headers = {"X-Agent-Key": api_key}
+ last_metrics = 0
+ last_update_chk = 0
+
+ log(f"Agent v{AGENT_VERSION} (Windows) running. Polling {jarvis_url} every {heartbeat_every}s.")
while True:
now = time.time()
@@ -330,17 +512,23 @@ def main():
{"command_id": cmd["id"], "success": result.get("success", False), "result": result},
headers, ssl_verify=ssl_verify)
+ # Self-update check
+ if now - last_update_chk >= update_interval:
+ last_update_chk = now
+ self_update(cfg)
+
+ # Push metrics
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
+ api_post(f"{jarvis_url}/api/agent/metrics",
+ {"type": "system", "data": metrics}, headers, ssl_verify=ssl_verify)
+ last_metrics = now
except Exception as e:
log(f"[ERROR] Loop error: {e}")
time.sleep(heartbeat_every)
+
if __name__ == "__main__":
main()
diff --git a/public_html/agent/jarvis-agent-windows.py.sha256 b/public_html/agent/jarvis-agent-windows.py.sha256
new file mode 100644
index 0000000..f4a888c
--- /dev/null
+++ b/public_html/agent/jarvis-agent-windows.py.sha256
@@ -0,0 +1 @@
+feadcb033426838f0d9e87df933fc3cd6b09b0d74eea7dc697b29a12421a1f2d jarvis-agent-windows.py
diff --git a/public_html/agent/jarvis-agent.py b/public_html/agent/jarvis-agent.py
index 8dcfe1e..4494c7a 100644
--- a/public_html/agent/jarvis-agent.py
+++ b/public_html/agent/jarvis-agent.py
@@ -23,7 +23,7 @@ from pathlib import Path
CONFIG_PATH = "/etc/jarvis-agent/config.json"
STATE_PATH = "/var/lib/jarvis-agent/state.json"
-AGENT_VERSION = "3.0" # Phase 4: screenshot + sysinfo commands
+AGENT_VERSION = "3.1"
# ── Config helpers ────────────────────────────────────────────────────────────
@@ -119,12 +119,6 @@ def detect_capabilities(cfg: dict) -> list:
# Check for Home Assistant
if os.path.exists("/etc/homeassistant") or os.path.exists("/config/configuration.yaml"):
caps.append("homeassistant")
- # Phase 4: screenshot capability
- import shutil as _shutil
- if (_shutil.which("scrot") or _shutil.which("import") or
- _shutil.which("fbcat") or _shutil.which("convert")):
- caps.append("screenshot")
- caps.append("sysinfo")
return caps
def register(cfg: dict, state: dict) -> str:
@@ -304,198 +298,6 @@ def collect_proxmox_metrics(cfg: dict) -> dict | None:
except Exception as e:
return {"error": str(e)}
-# ── Screenshot / Vision helpers ───────────────────────────────────────────────
-
-def _take_screenshot(cmd_data: dict) -> dict:
- """
- Attempts to capture a screenshot using available tools.
- For headless servers, falls back to a rich text system snapshot.
- Returns base64-encoded PNG and metadata.
- """
- import base64, tempfile, shutil
-
- tmp = tempfile.mktemp(suffix=".png")
- width = height = 0
- method = "unknown"
-
- # 1. Try scrot (X11 desktop)
- if shutil.which("scrot") and os.environ.get("DISPLAY"):
- try:
- r = subprocess.run(["scrot", "-z", tmp], capture_output=True, timeout=10)
- if r.returncode == 0 and os.path.exists(tmp):
- method = "scrot"
- except Exception:
- pass
-
- # 2. Try import (ImageMagick X11)
- if method == "unknown" and shutil.which("import") and os.environ.get("DISPLAY"):
- try:
- r = subprocess.run(["import", "-window", "root", tmp], capture_output=True, timeout=10)
- if r.returncode == 0 and os.path.exists(tmp):
- method = "import"
- except Exception:
- pass
-
- # 3. Try fbcat (Linux framebuffer — headless VMs with framebuffer)
- if method == "unknown" and shutil.which("fbcat") and os.path.exists("/dev/fb0"):
- try:
- ppm = tempfile.mktemp(suffix=".ppm")
- r = subprocess.run(["fbcat", "-s", "/dev/fb0"], stdout=open(ppm, "wb"),
- stderr=subprocess.PIPE, timeout=10)
- if r.returncode == 0 and shutil.which("convert"):
- subprocess.run(["convert", ppm, tmp], capture_output=True, timeout=10)
- os.unlink(ppm)
- if os.path.exists(tmp):
- method = "framebuffer"
- except Exception:
- pass
-
- # 4. Headless fallback: build a PNG system dashboard from text stats
- if method == "unknown":
- try:
- result = _render_sysinfo_png(tmp)
- if result:
- method = "sysinfo_render"
- except Exception:
- pass
-
- if method == "unknown" or not os.path.exists(tmp):
- # Last resort: return text snapshot only
- snap = _sysinfo_snapshot()
- snap["screenshot_available"] = False
- snap["method"] = "text_only"
- return snap
-
- # Read image
- try:
- with open(tmp, "rb") as f:
- raw = f.read()
- b64 = base64.b64encode(raw).decode()
- fsize = len(raw)
- os.unlink(tmp)
-
- # Try to get dimensions via file command
- try:
- r = subprocess.run(["identify", "-format", "%wx%h", tmp],
- capture_output=True, text=True, timeout=5)
- if "x" in r.stdout:
- w, h = r.stdout.strip().split("x", 1)
- width, height = int(w), int(h)
- except Exception:
- pass
-
- return {
- "success": True,
- "method": method,
- "image_b64": b64,
- "image_mime": "image/png",
- "file_size": fsize,
- "width": width,
- "height": height,
- "hostname": socket.gethostname(),
- }
- except Exception as e:
- return {"success": False, "error": str(e), "method": method}
-
-
-def _render_sysinfo_png(out_path: str) -> bool:
- """Render a system info text snapshot as a PNG using ansi2image or ImageMagick."""
- import shutil
- snap = _build_sysinfo_text()
- # Try convert (ImageMagick) to render text → PNG
- if shutil.which("convert"):
- try:
- r = subprocess.run([
- "convert",
- "-size", "900x600", "xc:#0a0f14",
- "-font", "Courier-New",
- "-pointsize", "13",
- "-fill", "#00d4ff",
- "-annotate", "+20+30", snap[:3000],
- out_path,
- ], capture_output=True, timeout=15)
- return r.returncode == 0 and os.path.exists(out_path)
- except Exception:
- pass
- return False
-
-
-def _build_sysinfo_text() -> str:
- """Build a rich text system snapshot for headless machines."""
- lines = [f"JARVIS FIELD STATION — {socket.gethostname()}",
- f"Timestamp: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}",
- "─" * 60]
- try:
- # CPU / mem / disk
- with open("/proc/loadavg") as f:
- load = f.read().split()[:3]
- lines.append(f"Load avg: {' '.join(load)}")
- except Exception:
- pass
- try:
- with open("/proc/meminfo") as f:
- minfo = {l.split(":")[0].strip(): int(l.split()[1]) for l in f if ":" in l}
- total = minfo.get("MemTotal", 0)
- avail = minfo.get("MemAvailable", 0)
- used = total - avail
- lines.append(f"Memory: {used//1024}MB used / {total//1024}MB total")
- except Exception:
- pass
- try:
- r = subprocess.run(["df", "-h", "/"], capture_output=True, text=True, timeout=5)
- lines.append("Disk:\n" + r.stdout.strip())
- except Exception:
- pass
- try:
- r = subprocess.run(["ps", "aux", "--sort=-%cpu"], capture_output=True, text=True, timeout=5)
- lines.append("Top processes:\n" + "\n".join(r.stdout.splitlines()[1:8]))
- except Exception:
- pass
- try:
- r = subprocess.run(["ss", "-tlnp"], capture_output=True, text=True, timeout=5)
- lines.append("Listening ports:\n" + r.stdout.strip()[:500])
- except Exception:
- pass
- return "\n".join(lines)
-
-
-def _sysinfo_snapshot() -> dict:
- """Return structured system snapshot (no image) for text-based analysis."""
- data = {"success": True, "hostname": socket.gethostname(),
- "snapshot_type": "text", "screenshot_available": False}
- try:
- with open("/proc/loadavg") as f:
- parts = f.read().split()
- data["load_1m"], data["load_5m"], data["load_15m"] = parts[0], parts[1], parts[2]
- except Exception:
- pass
- try:
- with open("/proc/meminfo") as f:
- m = {l.split(":")[0].strip(): int(l.split()[1])
- for l in f if ":" in l and len(l.split()) >= 2}
- data["mem_total_mb"] = m.get("MemTotal", 0) // 1024
- data["mem_avail_mb"] = m.get("MemAvailable", 0) // 1024
- data["mem_used_mb"] = data["mem_total_mb"] - data["mem_avail_mb"]
- except Exception:
- pass
- try:
- r = subprocess.run(["df", "-h", "/"], capture_output=True, text=True, timeout=5)
- data["disk"] = r.stdout.splitlines()[1] if r.stdout else ""
- except Exception:
- pass
- try:
- r = subprocess.run(["ps", "aux", "--sort=-%cpu"], capture_output=True, text=True, timeout=5)
- data["top_procs"] = r.stdout.splitlines()[1:8]
- except Exception:
- pass
- try:
- r = subprocess.run(["ss", "-tlnp"], capture_output=True, text=True, timeout=5)
- data["listening_ports"] = r.stdout.strip()[:800]
- except Exception:
- pass
- return data
-
-
# ── Command execution ─────────────────────────────────────────────────────────
def execute_command(cmd: dict) -> dict:
@@ -525,25 +327,17 @@ def execute_command(cmd: dict) -> dict:
return {"success": r.returncode == 0, "output": r.stdout}
elif cmd_type == "update":
- _cfg = load_config()
- updated = self_update(_cfg)
+ updated = self_update(cfg)
return {"success": True, "updated": updated}
elif cmd_type == "shell":
- # Guard reads LOCAL config, not the server-supplied payload
- _cfg = load_config()
- if not _cfg.get("allow_shell_commands", False):
- return {"success": False, "error": "Shell commands not enabled in agent config"}
+ # 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]}
- elif cmd_type == "screenshot":
- return _take_screenshot(cmd_data)
-
- elif cmd_type == "sysinfo":
- return _sysinfo_snapshot()
-
else:
return {"success": False, "error": f"Unknown command type: {cmd_type}"}
@@ -565,18 +359,15 @@ def main():
poll_interval = int(cfg.get("poll_interval", 30))
heartbeat_every = int(cfg.get("heartbeat_every", 10))
- # Always re-register on startup to refresh capabilities, version, and IP.
- # Server does an UPDATE when agent_id already exists, so api_key is preserved.
+ # Register if no API key yet
api_key = state.get("api_key", "")
- registered_key = register(cfg, state)
- if registered_key:
- api_key = registered_key
- elif not api_key:
- while 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)
+ 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
@@ -633,9 +424,7 @@ def main():
# ── Self-update ────────────────────────────────────────────────────────────────
def self_update(cfg: dict) -> bool:
- """Check JARVIS server for a newer version of this script.
- Verifies SHA-256 hash from .sha256 before replacing."""
- import hashlib
+ """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)
@@ -643,37 +432,14 @@ def self_update(cfg: dict) -> bool:
return False
script_path = os.path.abspath(__file__)
try:
- # Download expected hash first
- hash_url = update_url + ".sha256"
- req_hash = urllib.request.Request(hash_url)
- req_hash.add_header("User-Agent", "JARVIS-Agent/1.0")
- if _host_header:
- req_hash.add_header("Host", _host_header)
- try:
- with urllib.request.urlopen(req_hash, timeout=10) as resp:
- expected_hash = resp.read().decode().strip().split()[0]
- except Exception:
- expected_hash = None
-
- # Download new script
req = urllib.request.Request(update_url)
req.add_header("User-Agent", "JARVIS-Agent/1.0")
- if _host_header:
- req.add_header("Host", _host_header)
with urllib.request.urlopen(req, timeout=30) as resp:
new_content = resp.read()
-
- # Verify hash if available — abort if mismatch
- if expected_hash:
- actual_hash = hashlib.sha256(new_content).hexdigest()
- if actual_hash != expected_hash:
- print(f"[JARVIS] Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting", flush=True)
- return False
-
with open(script_path, "rb") as f:
current = f.read()
if new_content != current:
- print(f"[JARVIS] Update verified — replacing {script_path} and restarting...", flush=True)
+ 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)
diff --git a/public_html/agent/jarvis-agent.py.sha256 b/public_html/agent/jarvis-agent.py.sha256
index 4cde60a..b7f4083 100644
--- a/public_html/agent/jarvis-agent.py.sha256
+++ b/public_html/agent/jarvis-agent.py.sha256
@@ -1 +1 @@
-aa05371d8610a5fd89f397b7feda90fd93acc169a5c910c2969b6319a189da25 /home/jarvis.orbishosting.com/public_html/agent/jarvis-agent.py
+bccfeed43d7fbcd5f002656e866432b8aaa5035bc797f855727f50147f609427 jarvis-agent.py