gemini-computer-use 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +196 -0
- package/bin/cli.js +27 -0
- package/core/__init__.py +0 -0
- package/core/agent.py +57 -0
- package/core/agent_harness.py +60 -0
- package/core/code_cache.py +232 -0
- package/core/hooks.py +61 -0
- package/core/policies.py +99 -0
- package/core/schemas.py +49 -0
- package/core/server.py +69 -0
- package/core/subagents.py +55 -0
- package/core/triggers.py +70 -0
- package/core/vitals.py +117 -0
- package/install.ps1 +339 -0
- package/install.sh +777 -0
- package/package.json +41 -0
- package/skills/orchestrator.md +14 -0
package/core/policies.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from typing import List, Optional, Callable, Any, Awaitable
|
|
4
|
+
from google.antigravity import types
|
|
5
|
+
from google.antigravity.hooks import policy
|
|
6
|
+
from google.antigravity.hooks.policy import Policy, Decision
|
|
7
|
+
|
|
8
|
+
ALLOWED_WORKSPACES = [
|
|
9
|
+
"/root/agy-gdrive-runner",
|
|
10
|
+
"/home/lev/MyProjects/antigravity-mesh",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
SAFE_COMMAND_PREFIXES = [
|
|
14
|
+
"git status",
|
|
15
|
+
"git log",
|
|
16
|
+
"git diff",
|
|
17
|
+
"systemctl status",
|
|
18
|
+
"uptime",
|
|
19
|
+
"ls",
|
|
20
|
+
"ps",
|
|
21
|
+
"cat",
|
|
22
|
+
"echo",
|
|
23
|
+
"whoami",
|
|
24
|
+
"uname",
|
|
25
|
+
"df",
|
|
26
|
+
"free",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
DESTRUCTIVE_COMMAND_PATTERNS = [
|
|
30
|
+
re.compile(r"\breboot\b"),
|
|
31
|
+
re.compile(r"\bshutdown\b"),
|
|
32
|
+
re.compile(r"\bsystemctl\s+(stop|disable|mask|restart)\b"),
|
|
33
|
+
re.compile(r"\brm\s+"),
|
|
34
|
+
re.compile(r"\bpkill\b"),
|
|
35
|
+
re.compile(r"\bkill\s+-9\b"),
|
|
36
|
+
re.compile(r"\bmkfs\b"),
|
|
37
|
+
re.compile(r"\bdd\s+"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def validate_workspace_path(path: str, allowed_workspaces: Optional[List[str]] = None) -> bool:
|
|
41
|
+
workspaces = allowed_workspaces or ALLOWED_WORKSPACES
|
|
42
|
+
abs_path = os.path.abspath(path)
|
|
43
|
+
for ws in workspaces:
|
|
44
|
+
ws_abs = os.path.abspath(ws)
|
|
45
|
+
if abs_path == ws_abs or abs_path.startswith(ws_abs.rstrip("/") + "/"):
|
|
46
|
+
return True
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
def _extract_command(args: dict) -> str:
|
|
50
|
+
if not isinstance(args, dict):
|
|
51
|
+
return ""
|
|
52
|
+
return str(args.get("command") or args.get("CommandLine") or "").strip()
|
|
53
|
+
|
|
54
|
+
def is_safe_command(args: dict) -> bool:
|
|
55
|
+
cmd = _extract_command(args)
|
|
56
|
+
if not cmd:
|
|
57
|
+
return False
|
|
58
|
+
return any(cmd.startswith(prefix) for prefix in SAFE_COMMAND_PREFIXES)
|
|
59
|
+
|
|
60
|
+
def is_destructive_command(args: dict) -> bool:
|
|
61
|
+
cmd = _extract_command(args)
|
|
62
|
+
if not cmd:
|
|
63
|
+
return False
|
|
64
|
+
return any(pat.search(cmd) for pat in DESTRUCTIVE_COMMAND_PATTERNS)
|
|
65
|
+
|
|
66
|
+
async def default_deny_handler(tool_call: types.ToolCall) -> bool:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def get_mesh_policies(
|
|
70
|
+
allowed_workspaces: Optional[List[str]] = None,
|
|
71
|
+
ask_user_handler: Optional[Callable[[types.ToolCall], Awaitable[bool]]] = None,
|
|
72
|
+
) -> List[Policy]:
|
|
73
|
+
handler = ask_user_handler or default_deny_handler
|
|
74
|
+
policies: List[Policy] = []
|
|
75
|
+
|
|
76
|
+
# 1. Ask user for destructive commands
|
|
77
|
+
policies.append(
|
|
78
|
+
policy.ask_user(
|
|
79
|
+
"run_command",
|
|
80
|
+
when=is_destructive_command,
|
|
81
|
+
handler=handler,
|
|
82
|
+
name="confirm_destructive",
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# 2. Allow safe commands without confirmation
|
|
87
|
+
policies.append(
|
|
88
|
+
policy.allow(
|
|
89
|
+
"run_command",
|
|
90
|
+
when=is_safe_command,
|
|
91
|
+
name="allow_safe_commands",
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return policies
|
|
96
|
+
|
|
97
|
+
async def evaluate_policies(policies: List[Policy], tool_call: types.ToolCall) -> types.HookResult:
|
|
98
|
+
hook = policy._PolicyDecideHook(policies)
|
|
99
|
+
return await hook.run(None, tool_call)
|
package/core/schemas.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from pydantic import BaseModel, Field, field_validator
|
|
4
|
+
|
|
5
|
+
class NodeVitals(BaseModel):
|
|
6
|
+
hostname: str
|
|
7
|
+
ip: str
|
|
8
|
+
is_online: bool
|
|
9
|
+
cpu_load_1m: float
|
|
10
|
+
ram_used_mb: int
|
|
11
|
+
ram_total_mb: int
|
|
12
|
+
ram_usage_pct: float
|
|
13
|
+
disk_free_gb: float
|
|
14
|
+
disk_total_gb: float
|
|
15
|
+
uptime: str
|
|
16
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
17
|
+
|
|
18
|
+
@field_validator("ram_usage_pct")
|
|
19
|
+
@classmethod
|
|
20
|
+
def validate_ram_usage_pct(cls, v: float) -> float:
|
|
21
|
+
if not (0.0 <= v <= 100.0):
|
|
22
|
+
raise ValueError(f"ram_usage_pct must be between 0.0 and 100.0, got {v}")
|
|
23
|
+
return v
|
|
24
|
+
|
|
25
|
+
class ToolExecutionResult(BaseModel):
|
|
26
|
+
command: str
|
|
27
|
+
target_node: str
|
|
28
|
+
exit_code: int
|
|
29
|
+
duration_seconds: float
|
|
30
|
+
stdout: str
|
|
31
|
+
stderr: Optional[str] = None
|
|
32
|
+
is_error: bool
|
|
33
|
+
error_summary: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
class MeshHealthSnapshot(BaseModel):
|
|
36
|
+
overall_healthy: bool
|
|
37
|
+
healthy_node_count: int
|
|
38
|
+
total_node_count: int
|
|
39
|
+
nodes: List[NodeVitals]
|
|
40
|
+
warnings: List[str] = Field(default_factory=list)
|
|
41
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
42
|
+
|
|
43
|
+
class TaskDispatchPlan(BaseModel):
|
|
44
|
+
task_id: str
|
|
45
|
+
title: str
|
|
46
|
+
target_nodes: List[str]
|
|
47
|
+
ordered_steps: List[str]
|
|
48
|
+
rollback_steps: List[str]
|
|
49
|
+
dry_run_verified: bool = False
|
package/core/server.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import socket
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from core.schemas import NodeVitals
|
|
6
|
+
|
|
7
|
+
def get_coordinator_vitals() -> NodeVitals:
|
|
8
|
+
hostname = socket.gethostname()
|
|
9
|
+
ip = "127.0.0.1"
|
|
10
|
+
try:
|
|
11
|
+
load = os.getloadavg()[0]
|
|
12
|
+
except Exception:
|
|
13
|
+
load = 0.0
|
|
14
|
+
|
|
15
|
+
total_mb = 1
|
|
16
|
+
used_mb = 0
|
|
17
|
+
try:
|
|
18
|
+
with open("/proc/meminfo", "r") as f:
|
|
19
|
+
lines = f.readlines()
|
|
20
|
+
mem = {}
|
|
21
|
+
for line in lines:
|
|
22
|
+
parts = line.split(":")
|
|
23
|
+
if len(parts) == 2:
|
|
24
|
+
k = parts[0].strip()
|
|
25
|
+
v = parts[1].strip().split()[0]
|
|
26
|
+
if k in ["MemTotal", "MemAvailable"]:
|
|
27
|
+
mem[k] = int(v) // 1024
|
|
28
|
+
if "MemTotal" in mem and "MemAvailable" in mem:
|
|
29
|
+
total_mb = mem["MemTotal"]
|
|
30
|
+
avail_mb = mem["MemAvailable"]
|
|
31
|
+
used_mb = max(0, total_mb - avail_mb)
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
usage_pct = round((used_mb / total_mb) * 100.0, 2) if total_mb > 0 else 0.0
|
|
36
|
+
usage_pct = min(100.0, max(0.0, usage_pct))
|
|
37
|
+
|
|
38
|
+
total_disk_gb = 1.0
|
|
39
|
+
free_disk_gb = 0.0
|
|
40
|
+
try:
|
|
41
|
+
d = shutil.disk_usage("/")
|
|
42
|
+
total_disk_gb = round(d.total / (1024**3), 2)
|
|
43
|
+
free_disk_gb = round(d.free / (1024**3), 2)
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
uptime_str = "unknown"
|
|
48
|
+
try:
|
|
49
|
+
with open("/proc/uptime", "r") as f:
|
|
50
|
+
up_secs = float(f.read().split()[0])
|
|
51
|
+
hours = int(up_secs // 3600)
|
|
52
|
+
mins = int((up_secs % 3600) // 60)
|
|
53
|
+
uptime_str = f"{hours}h {mins}m"
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
return NodeVitals(
|
|
58
|
+
hostname=hostname,
|
|
59
|
+
ip=ip,
|
|
60
|
+
is_online=True,
|
|
61
|
+
cpu_load_1m=round(load, 2),
|
|
62
|
+
ram_used_mb=used_mb,
|
|
63
|
+
ram_total_mb=total_mb,
|
|
64
|
+
ram_usage_pct=usage_pct,
|
|
65
|
+
disk_free_gb=free_disk_gb,
|
|
66
|
+
disk_total_gb=total_disk_gb,
|
|
67
|
+
uptime=uptime_str,
|
|
68
|
+
timestamp=datetime.now(timezone.utc),
|
|
69
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from google.antigravity import types
|
|
3
|
+
|
|
4
|
+
coordinator_worker = types.SubagentConfig(
|
|
5
|
+
name="coordinator_worker",
|
|
6
|
+
description="Autonomous worker for coordinator tasks, executing shell commands and modifying files.",
|
|
7
|
+
capabilities=types.SubagentCapabilities(
|
|
8
|
+
agent_behavior=types.AgentBehavior.AUTONOMOUS,
|
|
9
|
+
enabled_tools=[
|
|
10
|
+
types.BuiltinTools.RUN_COMMAND,
|
|
11
|
+
types.BuiltinTools.VIEW_FILE,
|
|
12
|
+
types.BuiltinTools.EDIT_FILE,
|
|
13
|
+
types.BuiltinTools.CREATE_FILE,
|
|
14
|
+
types.BuiltinTools.SEARCH_DIR,
|
|
15
|
+
types.BuiltinTools.FIND_FILE,
|
|
16
|
+
types.BuiltinTools.LIST_DIR,
|
|
17
|
+
],
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
remote_node_worker = types.SubagentConfig(
|
|
22
|
+
name="remote_node_worker",
|
|
23
|
+
description="Autonomous worker for dispatching commands and collecting vitals on remote mesh nodes.",
|
|
24
|
+
capabilities=types.SubagentCapabilities(
|
|
25
|
+
agent_behavior=types.AgentBehavior.AUTONOMOUS,
|
|
26
|
+
enabled_tools=[
|
|
27
|
+
types.BuiltinTools.RUN_COMMAND,
|
|
28
|
+
types.BuiltinTools.VIEW_FILE,
|
|
29
|
+
],
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
code_researcher = types.SubagentConfig(
|
|
34
|
+
name="code_researcher",
|
|
35
|
+
description="Read-only research worker for exploring codebase, search, and reading files.",
|
|
36
|
+
capabilities=types.SubagentCapabilities(
|
|
37
|
+
agent_behavior=types.AgentBehavior.AUTONOMOUS,
|
|
38
|
+
enabled_tools=[
|
|
39
|
+
types.BuiltinTools.VIEW_FILE,
|
|
40
|
+
types.BuiltinTools.SEARCH_DIR,
|
|
41
|
+
types.BuiltinTools.FIND_FILE,
|
|
42
|
+
types.BuiltinTools.LIST_DIR,
|
|
43
|
+
],
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def get_mesh_subagents() -> List[types.SubagentConfig]:
|
|
48
|
+
return [coordinator_worker, remote_node_worker, code_researcher]
|
|
49
|
+
|
|
50
|
+
def get_root_capabilities_config() -> types.CapabilitiesConfig:
|
|
51
|
+
return types.CapabilitiesConfig(
|
|
52
|
+
enable_subagents=True,
|
|
53
|
+
max_subagent_depth=2,
|
|
54
|
+
allowed_subagents=["coordinator_worker", "remote_node_worker", "code_researcher"],
|
|
55
|
+
)
|
package/core/triggers.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Callable, Awaitable, List, Optional, Sequence
|
|
4
|
+
from google.antigravity.triggers import (
|
|
5
|
+
Trigger,
|
|
6
|
+
TriggerContext,
|
|
7
|
+
FileChange,
|
|
8
|
+
FileChangeKind,
|
|
9
|
+
every,
|
|
10
|
+
on_file_change,
|
|
11
|
+
)
|
|
12
|
+
from core.schemas import NodeVitals
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
async def default_vitals_fetcher() -> List[NodeVitals]:
|
|
17
|
+
"""Default local fetcher when no custom fetcher is provided."""
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
async def check_mesh_health(
|
|
21
|
+
ctx: TriggerContext,
|
|
22
|
+
vitals_fetcher: Optional[Callable[[], Awaitable[List[NodeVitals]]]] = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
fetcher = vitals_fetcher or default_vitals_fetcher
|
|
25
|
+
try:
|
|
26
|
+
nodes = await fetcher()
|
|
27
|
+
except Exception as e:
|
|
28
|
+
logger.error(f"Error fetching vitals in watchdog: {e}")
|
|
29
|
+
await ctx.send(f"[WATCHDOG ALERT] Failed to collect mesh vitals: {e}")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
for node in nodes:
|
|
33
|
+
if not node.is_online:
|
|
34
|
+
await ctx.send(
|
|
35
|
+
f"[WATCHDOG ALERT] Node '{node.hostname}' ({node.ip}) is offline! Check mesh tunnel and host status."
|
|
36
|
+
)
|
|
37
|
+
elif node.ram_usage_pct > 90.0:
|
|
38
|
+
await ctx.send(
|
|
39
|
+
f"[WATCHDOG ALERT] Node '{node.hostname}' exceeded critical memory threshold ({node.ram_usage_pct:.1f}%). Diagnostic required."
|
|
40
|
+
)
|
|
41
|
+
elif node.cpu_load_1m > 10.0:
|
|
42
|
+
await ctx.send(
|
|
43
|
+
f"[WATCHDOG ALERT] Node '{node.hostname}' high load average: {node.cpu_load_1m}."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async def handle_config_file_change(
|
|
47
|
+
ctx: TriggerContext,
|
|
48
|
+
changes: Sequence[FileChange],
|
|
49
|
+
reload_callback: Optional[Callable[[Sequence[FileChange]], Awaitable[None]]] = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
if reload_callback:
|
|
52
|
+
await reload_callback(changes)
|
|
53
|
+
|
|
54
|
+
paths_str = ", ".join([str(c.path) for c in changes])
|
|
55
|
+
await ctx.send(f"[CONFIG RELOAD] Config reloaded due to change in {paths_str}")
|
|
56
|
+
|
|
57
|
+
def create_mesh_watchdog_trigger(
|
|
58
|
+
interval_seconds: float = 60.0,
|
|
59
|
+
vitals_fetcher: Optional[Callable[[], Awaitable[List[NodeVitals]]]] = None,
|
|
60
|
+
) -> Trigger:
|
|
61
|
+
return every(interval_seconds, lambda ctx: check_mesh_health(ctx, vitals_fetcher))
|
|
62
|
+
|
|
63
|
+
def create_config_trigger(
|
|
64
|
+
path: str = "server_facts.json",
|
|
65
|
+
reload_callback: Optional[Callable[[Sequence[FileChange]], Awaitable[None]]] = None,
|
|
66
|
+
) -> Trigger:
|
|
67
|
+
return on_file_change(
|
|
68
|
+
path,
|
|
69
|
+
lambda ctx, changes: handle_config_file_change(ctx, changes, reload_callback),
|
|
70
|
+
)
|
package/core/vitals.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import shutil
|
|
4
|
+
import socket
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
|
|
8
|
+
def get_host_vitals() -> Dict[str, Any]:
|
|
9
|
+
hostname = socket.gethostname()
|
|
10
|
+
system = platform.system() # 'Linux', 'Darwin', 'Windows'
|
|
11
|
+
|
|
12
|
+
# CPU load average (Unix)
|
|
13
|
+
cpu_load = {"1m": 0.0, "5m": 0.0, "15m": 0.0}
|
|
14
|
+
try:
|
|
15
|
+
if hasattr(os, "getloadavg"):
|
|
16
|
+
load = os.getloadavg()
|
|
17
|
+
cpu_load = {
|
|
18
|
+
"1m": round(load[0], 2),
|
|
19
|
+
"5m": round(load[1], 2),
|
|
20
|
+
"15m": round(load[2], 2),
|
|
21
|
+
}
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
# RAM metrics
|
|
26
|
+
total_mb = 0.0
|
|
27
|
+
free_mb = 0.0
|
|
28
|
+
|
|
29
|
+
if system == "Linux":
|
|
30
|
+
total_kb, avail_kb = 0, 0
|
|
31
|
+
try:
|
|
32
|
+
with open("/proc/meminfo", "r") as f:
|
|
33
|
+
for line in f:
|
|
34
|
+
parts = line.split(":")
|
|
35
|
+
key = parts[0].strip()
|
|
36
|
+
if key == "MemTotal":
|
|
37
|
+
total_kb = int(parts[1].split()[0])
|
|
38
|
+
elif key == "MemAvailable":
|
|
39
|
+
avail_kb = int(parts[1].split()[0])
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
total_mb = round(total_kb / 1024, 1)
|
|
43
|
+
free_mb = round(avail_kb / 1024, 1)
|
|
44
|
+
elif system == "Darwin":
|
|
45
|
+
# macOS
|
|
46
|
+
try:
|
|
47
|
+
mem_bytes = int(subprocess.check_output(["sysctl", "-n", "hw.memsize"], text=True).strip())
|
|
48
|
+
total_mb = round(mem_bytes / (1024**2), 1)
|
|
49
|
+
vm = subprocess.check_output(["vm_stat"], text=True)
|
|
50
|
+
page_size = 4096
|
|
51
|
+
free_pages = 0
|
|
52
|
+
for line in vm.splitlines():
|
|
53
|
+
if "page size of" in line:
|
|
54
|
+
page_size = int(line.split()[-2])
|
|
55
|
+
elif "Pages free:" in line or "Pages speculative:" in line:
|
|
56
|
+
free_pages += int(line.split()[-1].replace(".", ""))
|
|
57
|
+
free_mb = round((free_pages * page_size) / (1024**2), 1)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
elif system == "Windows":
|
|
61
|
+
try:
|
|
62
|
+
import ctypes
|
|
63
|
+
class MEMORYSTATUSEX(ctypes.Structure):
|
|
64
|
+
_fields_ = [
|
|
65
|
+
("dwLength", ctypes.c_ulong),
|
|
66
|
+
("dwMemoryLoad", ctypes.c_ulong),
|
|
67
|
+
("ullTotalPhys", ctypes.c_ulonglong),
|
|
68
|
+
("ullAvailPhys", ctypes.c_ulonglong),
|
|
69
|
+
("ullTotalPageFile", ctypes.c_ulonglong),
|
|
70
|
+
("ullAvailPageFile", ctypes.c_ulonglong),
|
|
71
|
+
("ullTotalVirtual", ctypes.c_ulonglong),
|
|
72
|
+
("ullAvailVirtual", ctypes.c_ulonglong),
|
|
73
|
+
("sullAvailExtendedVirtual", ctypes.c_ulonglong),
|
|
74
|
+
]
|
|
75
|
+
stat = MEMORYSTATUSEX()
|
|
76
|
+
stat.dwLength = ctypes.sizeof(MEMORYSTATUSEX)
|
|
77
|
+
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat))
|
|
78
|
+
total_mb = round(stat.ullTotalPhys / (1024**2), 1)
|
|
79
|
+
free_mb = round(stat.ullAvailPhys / (1024**2), 1)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
used_mb = max(0.0, round(total_mb - free_mb, 1))
|
|
84
|
+
used_ram_pct = round((used_mb / total_mb) * 100.0, 1) if total_mb > 0 else 0.0
|
|
85
|
+
|
|
86
|
+
ram = {
|
|
87
|
+
"total_mb": total_mb,
|
|
88
|
+
"used_mb": used_mb,
|
|
89
|
+
"free_mb": free_mb,
|
|
90
|
+
"used_pct": used_ram_pct,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Disk metrics
|
|
94
|
+
root_path = "C:\\" if system == "Windows" else "/"
|
|
95
|
+
try:
|
|
96
|
+
du = shutil.disk_usage(root_path)
|
|
97
|
+
total_gb = round(du.total / (1024**3), 2)
|
|
98
|
+
used_gb = round(du.used / (1024**3), 2)
|
|
99
|
+
free_gb = round(du.free / (1024**3), 2)
|
|
100
|
+
used_disk_pct = round((du.used / du.total) * 100.0, 1) if du.total > 0 else 0.0
|
|
101
|
+
except Exception:
|
|
102
|
+
total_gb, used_gb, free_gb, used_disk_pct = 0.0, 0.0, 0.0, 0.0
|
|
103
|
+
|
|
104
|
+
disk = {
|
|
105
|
+
"total_gb": total_gb,
|
|
106
|
+
"used_gb": used_gb,
|
|
107
|
+
"free_gb": free_gb,
|
|
108
|
+
"used_pct": used_disk_pct,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"hostname": hostname,
|
|
113
|
+
"os": system,
|
|
114
|
+
"cpu_load": cpu_load,
|
|
115
|
+
"ram": ram,
|
|
116
|
+
"disk": disk,
|
|
117
|
+
}
|