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.
@@ -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)
@@ -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
+ )
@@ -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
+ }