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
+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()