Agent v3.1/3.0 — Mac agent, Windows self-update, all capabilities parity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 01:20:38 +00:00
parent 9e590dff29
commit 4c67efe715
11 changed files with 2127 additions and 571 deletions
+135
View File
@@ -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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>$SERVICE_LABEL</string>
<key>ProgramArguments</key>
<array>
<string>$PYTHON3</string>
<string>$INSTALL_DIR/jarvis-agent.py</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>JARVIS_CONFIG</key>
<string>$INSTALL_DIR/config.json</string>
<key>JARVIS_STATE</key>
<string>$INSTALL_DIR/state.json</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>$INSTALL_DIR/jarvis-agent.log</string>
<key>StandardErrorPath</key>
<string>$INSTALL_DIR/jarvis-agent.log</string>
</dict>
</plist>
PLISTEOF
# Load/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 ""
+576
View File
@@ -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()
+534
View File
@@ -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()
+15 -249
View File
@@ -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 <update_url>.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)
+45 -32
View File
@@ -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
<key>EnvironmentVariables</key>
<dict>
<key>JARVIS_CONFIG</key>
<string>$CONFIG_DIR/config.json</string>
<string>$INSTALL_DIR/config.json</string>
<key>JARVIS_STATE</key>
<string>$STATE_PATH</string>
<string>$INSTALL_DIR/state.json</string>
</dict>
<key>RunAtLoad</key>
<true/>
@@ -105,18 +113,23 @@ cat > "$PLIST_PATH" << PLISTEOF
</plist>
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 ""
+576
View File
@@ -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()
@@ -0,0 +1 @@
d0aeaab1686f788c9d46ffeaac57a42e3e82b80c2d7fac2be443c05cf70c87be jarvis-agent-mac.py
+228 -40
View File
@@ -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()
@@ -0,0 +1 @@
feadcb033426838f0d9e87df933fc3cd6b09b0d74eea7dc697b29a12421a1f2d jarvis-agent-windows.py
+15 -249
View File
@@ -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 <update_url>.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)
+1 -1
View File
@@ -1 +1 @@
aa05371d8610a5fd89f397b7feda90fd93acc169a5c910c2969b6319a189da25 /home/jarvis.orbishosting.com/public_html/agent/jarvis-agent.py
bccfeed43d7fbcd5f002656e866432b8aaa5035bc797f855727f50147f609427 jarvis-agent.py