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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lev (levra7)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# π Gemini Computer Use (Web & Mobile App) β FREE & Open Source
|
|
2
|
+
|
|
3
|
+
> **Turn your PC, Laptop, or Server into an autonomous execution node for Google Gemini (Web & Mobile App) via Native Model Context Protocol (MCP).**
|
|
4
|
+
|
|
5
|
+
[English](README.md) | [π·πΊ Π ΡΡΡΠΊΠ°Ρ Π²Π΅ΡΡΠΈΡ](#-ΡΡΡΡΠΊΠ°Ρ-Π²Π΅ΡΡΠΈΡ)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## π‘ What is Gemini Computer Use?
|
|
10
|
+
|
|
11
|
+
**Gemini Computer Use** is an open-source, free solution that gives **Google Gemini** direct, autonomous command-execution capability on your local machine, laptop, or remote cloud server directly from the chat interface of [gemini.google.com](https://gemini.google.com), the official Gemini mobile app, or Google AI Studio.
|
|
12
|
+
|
|
13
|
+
Unlike proprietary paid alternatives, this project uses the official open **Model Context Protocol (MCP)** specification from Google. It operates over a secure, outbound WebSocket reverse tunnel (no open router ports or public IPs required) or in a pure local standalone mode.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## β¨ Key Features
|
|
18
|
+
|
|
19
|
+
- π **100% Free**: Operates directly with the free tier of Google Gemini (Web & Mobile app).
|
|
20
|
+
- β‘ **Zero-Config One-Line Install**: Auto-detects device type (laptop, desktop, VPS, container), OS, and architecture.
|
|
21
|
+
- π **Two Deployment Modes**:
|
|
22
|
+
1. **Cloud Gateway + Reverse Tunnel (Recommended)**: Works behind NAT, firewalls, and dynamic IPs. No inbound ports required.
|
|
23
|
+
2. **Local Standalone**: Pure localhost FastMCP server on `localhost:8096` with zero external dependencies.
|
|
24
|
+
- π‘οΈ **Cryptographic Token Security**: Every tool invocation is secured by a 128-bit secret token (`?token=...`). Unauthorized requests are strictly blocked with HTTP 401.
|
|
25
|
+
- π‘ **Remote SSH Deployment**: Deploy nodes to remote Linux servers in one command directly from your terminal (`--ssh=user@host`).
|
|
26
|
+
- π **Native Background Autostart**: Installs and enables system background services (`systemd` on Linux, `launchd` on macOS, background task on Windows).
|
|
27
|
+
- π± **Mobile & Web Ready**: Prompt Gemini from your smartphone on the go β your home laptop or cloud server will execute tasks in real time!
|
|
28
|
+
- π» **Cross-Platform**: Fully supports Linux, macOS, and Windows.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## π οΈ Available MCP Tools
|
|
33
|
+
|
|
34
|
+
Once connected, Google Gemini gains direct native access to:
|
|
35
|
+
- `bash_exec(command)`: Execute shell commands, build code, run test suites, manage git repositories.
|
|
36
|
+
- `system_vitals()`: Real-time telemetry monitoring: CPU load, RAM usage, and disk space.
|
|
37
|
+
- `get_orchestration_skill()`: Dynamic delivery of infrastructure orchestrator personas and operational guidelines.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## π Quick Start (One-Liner Install)
|
|
42
|
+
|
|
43
|
+
### π§ Linux & π macOS (via curl)
|
|
44
|
+
```bash
|
|
45
|
+
curl -fsSL https://smart-server.online/install.sh | bash
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### πͺ Windows (via PowerShell)
|
|
49
|
+
```powershell
|
|
50
|
+
irm https://smart-server.online/install.ps1 | iex
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### π¦ Node.js / NPM (Cross-platform)
|
|
54
|
+
```bash
|
|
55
|
+
npx gemini-computer-use
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### π οΈ Or clone via Git
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/LevRa7/Computer-use-for-Gemini-App-Web.git
|
|
61
|
+
cd Computer-use-for-Gemini-App-Web
|
|
62
|
+
./install.sh --quick
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## π± Connecting to Google Gemini
|
|
68
|
+
|
|
69
|
+
1. Open **[gemini.google.com/spark/apps](https://gemini.google.com/spark/apps)** (or the Gemini mobile app).
|
|
70
|
+
2. Click **Add App** (or go to **Settings βοΈ β Tools / Extensions (MCP)**).
|
|
71
|
+
3. The MCP SSE URL is **automatically copied to your clipboard** during install β just paste it (**Ctrl+V**):
|
|
72
|
+
```text
|
|
73
|
+
https://<your-device-name>.smart-server.online/sse?token=<your_secret_token>
|
|
74
|
+
```
|
|
75
|
+
4. Done! You can now prompt Gemini:
|
|
76
|
+
- *"Check system resources using system_vitals"*
|
|
77
|
+
- *"Create a FastAPI application and run test coverage via bash_exec"*
|
|
78
|
+
- *"Git commit and push changes"*
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## π₯οΈ Deployment Modes
|
|
83
|
+
|
|
84
|
+
### Mode 1: Cloud Gateway + Tunnel (Recommended)
|
|
85
|
+
```bash
|
|
86
|
+
./install.sh --quick
|
|
87
|
+
```
|
|
88
|
+
Perfect for personal laptops and home workstations behind NAT. The connection is maintained via an outbound secure WebSocket tunnel.
|
|
89
|
+
|
|
90
|
+
### Mode 2: Local Standalone
|
|
91
|
+
```bash
|
|
92
|
+
./install.sh --mode=standalone --port=8096
|
|
93
|
+
```
|
|
94
|
+
Runs an isolated, local FastMCP server on `http://localhost:8096/sse` without cloud relaying.
|
|
95
|
+
|
|
96
|
+
### Mode 3: Remote SSH Install
|
|
97
|
+
```bash
|
|
98
|
+
./install.sh --ssh=user@my-remote-server.com
|
|
99
|
+
```
|
|
100
|
+
Automatically transfers files and configures the node on your remote server via SSH.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## π Security Architecture
|
|
105
|
+
|
|
106
|
+
- **Token-Gated Endpoints**: Every incoming request must contain the cryptographic token via `?token=...` or `Authorization: Bearer`.
|
|
107
|
+
- **Outbound-Only Tunnel**: Local nodes do not open listening public ports. The agent initiates an outbound WebSocket connection to the gateway.
|
|
108
|
+
- **Unprivileged Execution**: Agents execute in user-space by default without requiring root privileges.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## π License
|
|
113
|
+
|
|
114
|
+
This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
# π·πΊ Π ΡΡΡΠΊΠ°Ρ Π²Π΅ΡΡΠΈΡ
|
|
120
|
+
|
|
121
|
+
> **ΠΡΠ΅Π²ΡΠ°ΡΠΈΡΠ΅ ΡΠ²ΠΎΠΉ ΠΠ, Π½ΠΎΡΡΠ±ΡΠΊ ΠΈΠ»ΠΈ ΡΠ΅ΡΠ²Π΅Ρ Π² Π°Π²ΡΠΎΠ½ΠΎΠΌΠ½ΡΠΉ ΠΈΡΠΏΠΎΠ»Π½ΠΈΡΠ΅Π»ΡΠ½ΡΠΉ ΡΠ·Π΅Π» Π΄Π»Ρ Google Gemini (Web ΠΈ Mobile App) ΡΠ΅ΡΠ΅Π· Π½Π°ΡΠΈΠ²Π½ΡΠΉ ΠΏΡΠΎΡΠΎΠΊΠΎΠ» Model Context Protocol (MCP).**
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## π‘ Π§ΡΠΎ ΡΡΠΎ ΡΠ°ΠΊΠΎΠ΅?
|
|
126
|
+
|
|
127
|
+
**Gemini Computer Use** β ΡΡΠΎ ΠΎΡΠΊΡΡΡΠΎΠ΅ ΠΈ Π±Π΅ΡΠΏΠ»Π°ΡΠ½ΠΎΠ΅ ΡΠ΅ΡΠ΅Π½ΠΈΠ΅ Π΄Π»Ρ ΠΏΡΡΠΌΠΎΠ³ΠΎ ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ Π²Π°ΡΠΈΠΌ ΠΊΠΎΠΌΠΏΡΡΡΠ΅ΡΠΎΠΌ, Π½ΠΎΡΡΠ±ΡΠΊΠΎΠΌ ΠΈΠ»ΠΈ ΡΠ΄Π°Π»ΡΠ½Π½ΡΠΌ ΡΠ΅ΡΠ²Π΅ΡΠΎΠΌ ΠΈΠ· Π΄ΠΈΠ°Π»ΠΎΠ³ΠΎΠ²ΠΎΠ³ΠΎ ΠΎΠΊΠ½Π° **Google Gemini** (Π²ΠΊΠ»ΡΡΠ°Ρ Π²Π΅Π±-Π²Π΅ΡΡΠΈΡ [gemini.google.com](https://gemini.google.com), ΠΌΠΎΠ±ΠΈΠ»ΡΠ½ΠΎΠ΅ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Gemini ΠΈ Google AI Studio).
|
|
128
|
+
|
|
129
|
+
ΠΡΠΎΠ΅ΠΊΡ ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅Ρ ΠΎΡΠΈΡΠΈΠ°Π»ΡΠ½ΡΠΉ ΠΏΡΠΎΡΠΎΠΊΠΎΠ» **Model Context Protocol (MCP)** ΠΎΡ Google ΠΈ ΡΠ°Π±ΠΎΡΠ°Π΅Ρ ΠΏΠΎΠ²Π΅ΡΡ
Π·Π°ΡΠΈΡΡΠ½Π½ΠΎΠ³ΠΎ WebSocket-ΡΡΠ½Π½Π΅Π»Ρ ΠΈΠ»ΠΈ Π² Π»ΠΎΠΊΠ°Π»ΡΠ½ΠΎΠΌ Π°Π²ΡΠΎΠ½ΠΎΠΌΠ½ΠΎΠΌ ΡΠ΅ΠΆΠΈΠΌΠ΅.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## β¨ ΠΠ»ΡΡΠ΅Π²ΡΠ΅ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ
|
|
134
|
+
|
|
135
|
+
- π **100% ΠΠ΅ΡΠΏΠ»Π°ΡΠ½ΠΎ**: Π Π°Π±ΠΎΡΠ°Π΅Ρ ΡΠΎ ΡΡΠ°Π½Π΄Π°ΡΡΠ½ΡΠΌ Π±Π΅ΡΠΏΠ»Π°ΡΠ½ΡΠΌ ΡΠ°ΡΠΈΡΠΎΠΌ Google Gemini (Web ΠΈ Mobile App).
|
|
136
|
+
- β‘ **Π£ΡΡΠ°Π½ΠΎΠ²ΠΊΠ° Π² ΠΎΠ΄Π½Ρ ΠΊΠΎΠΌΠ°Π½Π΄Ρ (One-Line Setup)**: Π‘ΠΊΡΠΈΠΏΡ Π°Π²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈ ΠΎΠΏΡΠ΅Π΄Π΅Π»ΡΠ΅Ρ ΡΠΈΠΏ ΡΡΡΡΠΎΠΉΡΡΠ²Π° (Π½ΠΎΡΡΠ±ΡΠΊ/Π΄Π΅ΡΠΊΡΠΎΠΏ/VPS/ΡΠ΅ΡΠ²Π΅Ρ), ΠΠ‘ ΠΈ Π°ΡΡ
ΠΈΡΠ΅ΠΊΡΡΡΡ.
|
|
137
|
+
- π **ΠΠ²Π° ΡΠ΅ΠΆΠΈΠΌΠ° ΡΠ°Π±ΠΎΡΡ**:
|
|
138
|
+
1. **Cloud Gateway + Reverse Tunnel**: Π Π°Π±ΠΎΡΠ°Π΅Ρ ΠΈΠ·-Π·Π° Π»ΡΠ±ΠΎΠ³ΠΎ Β«ΡΠ΅ΡΠΎΠ³ΠΎΒ» IP, ΡΠΎΡΡΠ΅ΡΠ° ΠΈ NAT Π±Π΅Π· Π±Π΅Π»ΠΎΠ³ΠΎ IP ΠΈ ΠΏΡΠΎΠ±ΡΠΎΡΠ° ΠΏΠΎΡΡΠΎΠ².
|
|
139
|
+
2. **Local Standalone**: ΠΠΎΠ»Π½ΠΎΡΡΡΡ Π»ΠΎΠΊΠ°Π»ΡΠ½ΡΠΉ FastMCP-ΡΠ΅ΡΠ²Π΅Ρ Π½Π° `localhost:8096` Π±Π΅Π· Π²Π½Π΅ΡΠ½ΠΈΡ
Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡΠ΅ΠΉ.
|
|
140
|
+
- π‘οΈ **Π‘ΡΡΠΎΠ³Π°Ρ Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡΡ**: ΠΠ°ΠΆΠ΄ΡΠΉ Π·Π°ΠΏΡΠΎΡ ΠΊ ΠΈΠ½ΡΡΡΡΠΌΠ΅Π½ΡΠ°ΠΌ Π·Π°ΡΠΈΡΡΠ½ ΠΏΠ΅ΡΡΠΎΠ½Π°Π»ΡΠ½ΡΠΌ ΠΊΡΠΈΠΏΡΠΎΠ³ΡΠ°ΡΠΈΡΠ΅ΡΠΊΠΈΠΌ ΡΠΎΠΊΠ΅Π½ΠΎΠΌ (`?token=...`). ΠΠΎΡΡΡΠΏ ΠΏΠΎΡΡΠΎΡΠΎΠ½Π½ΠΈΡ
ΡΡΡΠΎΠ³ΠΎ ΠΎΡΡΠ΅ΠΊΠ°Π΅ΡΡΡ (HTTP 401).
|
|
141
|
+
- π‘ **Π£Π΄Π°Π»ΡΠ½Π½Π°Ρ ΡΡΡΠ°Π½ΠΎΠ²ΠΊΠ° ΡΠ΅ΡΠ΅Π· SSH**: Π Π°Π·Π²ΡΡΡΡΠ²Π°Π½ΠΈΠ΅ ΡΠ·Π»Π° Π½Π° ΡΠ΄Π°Π»ΡΠ½Π½ΠΎΠΌ ΡΠ΅ΡΠ²Π΅ΡΠ΅ ΠΎΠ΄Π½ΠΎΠΉ ΠΊΠΎΠΌΠ°Π½Π΄ΠΎΠΉ (`--ssh=user@host`).
|
|
142
|
+
- π **ΠΠ²ΡΠΎΠ·Π°ΠΏΡΡΠΊ**: ΠΠ½ΡΠ΅Π³ΡΠ°ΡΠΈΡ ΡΠΎ ΡΠ»ΡΠΆΠ±Π°ΠΌΠΈ Π°Π²ΡΠΎΠ·Π°ΠΏΡΡΠΊΠ° (`systemd` Π² Linux, `launchd` Π² macOS, Π°Π²ΡΠΎΠ·Π°ΠΏΡΡΠΊ Π² Windows).
|
|
143
|
+
- π± **Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ ΡΠΎ ΡΠΌΠ°ΡΡΡΠΎΠ½Π°**: ΠΠΈΡΠΈΡΠ΅ ΠΊΠΎΠΌΠ°Π½Π΄Ρ Π² ΠΌΠΎΠ±ΠΈΠ»ΡΠ½ΠΎΠΌ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ Gemini β Π²Π°ΡΠ° Π΄ΠΎΠΌΠ°ΡΠ½ΡΡ ΠΌΠ°ΡΠΈΠ½Π° Π²ΡΠΏΠΎΠ»Π½ΠΈΡ ΠΈΡ
Π² ΡΠ΅Π°Π»ΡΠ½ΠΎΠΌ Π²ΡΠ΅ΠΌΠ΅Π½ΠΈ!
|
|
144
|
+
- π» **ΠΡΠΎΡΡΠΏΠ»Π°ΡΡΠΎΡΠΌΠ΅Π½Π½ΠΎΡΡΡ**: ΠΠΎΠ»Π½Π°Ρ ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΊΠ° Linux, macOS ΠΈ Windows.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## π οΈ ΠΠΎΡΡΡΠΏΠ½ΡΠ΅ MCP-ΠΈΠ½ΡΡΡΡΠΌΠ΅Π½ΡΡ
|
|
149
|
+
|
|
150
|
+
- `bash_exec(command)` β Π°Π²ΡΠΎΠ½ΠΎΠΌΠ½ΠΎΠ΅ Π²ΡΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ ΠΊΠΎΠΌΠ°Π½Π΄ ΡΠ΅ΡΠΌΠΈΠ½Π°Π»Π°, ΠΊΠΎΠΌΠΏΠΈΠ»ΡΡΠΈΡ ΠΊΠΎΠ΄Π°, ΡΠ΅ΡΡΡ, git-ΠΎΠΏΠ΅ΡΠ°ΡΠΈΠΈ.
|
|
151
|
+
- `system_vitals()` β ΠΌΠΎΠ½ΠΈΡΠΎΡΠΈΠ½Π³ Π½Π°Π³ΡΡΠ·ΠΊΠΈ Π² ΡΠ΅Π°Π»ΡΠ½ΠΎΠΌ Π²ΡΠ΅ΠΌΠ΅Π½ΠΈ (CPU, RAM, Π΄ΠΈΡΠΊ).
|
|
152
|
+
- `get_orchestration_skill()` β ΡΠΈΡΡΠ΅ΠΌΠ½ΡΠ΅ ΠΏΡΠ°Π²ΠΈΠ»Π° ΠΈ ΡΠΊΠΈΠ»Π» Π°Π²ΡΠΎΠ½ΠΎΠΌΠ½ΠΎΠ³ΠΎ ΠΎΡΠΊΠ΅ΡΡΡΠ°ΡΠΎΡΠ° ΠΈΠ½ΡΡΠ°ΡΡΡΡΠΊΡΡΡΡ.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## π ΠΡΡΡΡΡΠΉ ΡΡΠ°ΡΡ (Π£ΡΡΠ°Π½ΠΎΠ²ΠΊΠ° Π² 1 ΠΊΠΎΠΌΠ°Π½Π΄Ρ)
|
|
157
|
+
|
|
158
|
+
#### π§ Linux & π macOS (ΡΠ΅ΡΠ΅Π· curl):
|
|
159
|
+
```bash
|
|
160
|
+
curl -fsSL https://smart-server.online/install.sh | bash
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### πͺ Windows (ΡΠ΅ΡΠ΅Π· PowerShell):
|
|
164
|
+
```powershell
|
|
165
|
+
irm https://smart-server.online/install.ps1 | iex
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### π¦ Node.js / NPM (ΠΡΠΎΡΡΠΏΠ»Π°ΡΡΠΎΡΠΌΠ΅Π½Π½ΠΎ):
|
|
169
|
+
```bash
|
|
170
|
+
npx gemini-computer-use
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### π οΈ ΠΠΈΠ±ΠΎ Π²ΡΡΡΠ½ΡΡ ΡΠ΅ΡΠ΅Π· Git:
|
|
174
|
+
```bash
|
|
175
|
+
git clone https://github.com/LevRa7/Computer-use-for-Gemini-App-Web.git
|
|
176
|
+
cd Computer-use-for-Gemini-App-Web
|
|
177
|
+
./install.sh --quick
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## π± ΠΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΠ΅ ΠΊ Google Gemini
|
|
183
|
+
|
|
184
|
+
1. ΠΡΠΊΡΠΎΠΉΡΠ΅ ΡΡΡΠ°Π½ΠΈΡΡ **[gemini.google.com/spark/apps](https://gemini.google.com/spark/apps)** (Π² Π±ΡΠ°ΡΠ·Π΅ΡΠ΅ ΠΈΠ»ΠΈ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ).
|
|
185
|
+
2. ΠΠ°ΠΆΠΌΠΈΡΠ΅ **ΠΠΎΠ±Π°Π²ΠΈΡΡ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅** (Add App / ΠΠ°ΡΡΡΠΎΠΉΠΊΠΈ β MCP).
|
|
186
|
+
3. Π‘ΡΡΠ»ΠΊΠ° Π½Π° Π²Π°Ρ MCP-ΡΠ΅ΡΠ²Π΅Ρ **Π°Π²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈ ΡΠΊΠΎΠΏΠΈΡΠΎΠ²Π°Π½Π° Π² Π±ΡΡΠ΅Ρ ΠΎΠ±ΠΌΠ΅Π½Π°** ΠΏΡΠΈ ΡΡΡΠ°Π½ΠΎΠ²ΠΊΠ΅ β ΠΏΡΠΎΡΡΠΎ Π²ΡΡΠ°Π²ΡΡΠ΅ Π΅Ρ (**Ctrl+V**):
|
|
187
|
+
```text
|
|
188
|
+
https://<ΠΈΠΌΡ-Π²Π°ΡΠ΅Π³ΠΎ-ΠΏΠΊ>.smart-server.online/sse?token=<Π²Π°Ρ_ΡΠ΅ΠΊΡΠ΅ΡΠ½ΡΠΉ_ΡΠΎΠΊΠ΅Π½>
|
|
189
|
+
```
|
|
190
|
+
4. ΠΠΎΡΠΎΠ²ΠΎ! Π’Π΅ΠΏΠ΅ΡΡ Gemini ΠΌΠΎΠΆΠ΅Ρ Π²ΡΠΏΠΎΠ»Π½ΡΡΡ ΠΊΠΎΠΌΠ°Π½Π΄Ρ ΠΏΡΡΠΌΠΎ Π½Π° Π²Π°ΡΠ΅ΠΉ ΠΌΠ°ΡΠΈΠ½Π΅.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## π ΠΠΈΡΠ΅Π½Π·ΠΈΡ
|
|
195
|
+
|
|
196
|
+
Π Π°ΡΠΏΡΠΎΡΡΡΠ°Π½ΡΠ΅ΡΡΡ ΠΏΠΎΠ΄ ΡΠ²ΠΎΠ±ΠΎΠ΄Π½ΠΎΠΉ Π»ΠΈΡΠ΅Π½Π·ΠΈΠ΅ΠΉ **MIT**. ΠΠΎΠ΄ΡΠΎΠ±Π½ΠΎΡΡΠΈ Π² ΡΠ°ΠΉΠ»Π΅ [LICENSE](LICENSE).
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawn } = require('child_process');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const isWindows = os.platform() === 'win32';
|
|
6
|
+
const rawArgs = process.argv.slice(2).map(arg => `"${arg.replace(/"/g, '\\"')}"`).join(' ');
|
|
7
|
+
|
|
8
|
+
console.log('π Launching Gemini Computer Use installer...');
|
|
9
|
+
|
|
10
|
+
if (isWindows) {
|
|
11
|
+
const psCmd = rawArgs
|
|
12
|
+
? `& { $s = Invoke-RestMethod https://smart-server.online/install.ps1; & ([scriptblock]::Create($s)) ${rawArgs} }`
|
|
13
|
+
: `irm https://smart-server.online/install.ps1 | iex`;
|
|
14
|
+
const ps = spawn('powershell.exe', ['-ExecutionPolicy', 'Bypass', '-Command', psCmd], {
|
|
15
|
+
stdio: 'inherit'
|
|
16
|
+
});
|
|
17
|
+
ps.on('exit', (code) => process.exit(code || 0));
|
|
18
|
+
} else {
|
|
19
|
+
const bashCmd = rawArgs
|
|
20
|
+
? `curl -fsSL https://smart-server.online/install.sh | bash -s -- ${rawArgs}`
|
|
21
|
+
: `curl -fsSL https://smart-server.online/install.sh | bash`;
|
|
22
|
+
const sh = spawn('bash', ['-c', bashCmd], {
|
|
23
|
+
stdio: 'inherit'
|
|
24
|
+
});
|
|
25
|
+
sh.on('exit', (code) => process.exit(code || 0));
|
|
26
|
+
}
|
|
27
|
+
|
package/core/__init__.py
ADDED
|
File without changes
|
package/core/agent.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import websockets
|
|
7
|
+
from core.vitals import get_host_vitals
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
10
|
+
logger = logging.getLogger("agy-agent")
|
|
11
|
+
|
|
12
|
+
GATEWAY_HOST = os.environ.get("MESH_GATEWAY", "smart-server.online")
|
|
13
|
+
USER = os.environ.get("MESH_USER", "levra7")
|
|
14
|
+
TOKEN = os.environ.get("MESH_TOKEN", "")
|
|
15
|
+
|
|
16
|
+
async def handle_tool_call(name: str, args: dict) -> dict:
|
|
17
|
+
if name == "system_vitals":
|
|
18
|
+
return get_host_vitals()
|
|
19
|
+
elif name == "bash_exec":
|
|
20
|
+
cmd = args.get("command", "")
|
|
21
|
+
logger.info(f"Executing bash_exec: {cmd}")
|
|
22
|
+
try:
|
|
23
|
+
proc = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
|
|
24
|
+
return {"exit_code": proc.returncode, "stdout": proc.stdout, "stderr": proc.stderr}
|
|
25
|
+
except subprocess.TimeoutExpired:
|
|
26
|
+
return {"exit_code": 124, "stdout": "", "stderr": "Command timed out after 60 seconds"}
|
|
27
|
+
except Exception as e:
|
|
28
|
+
return {"exit_code": 1, "stdout": "", "stderr": str(e)}
|
|
29
|
+
return {"error": f"Unknown tool {name}"}
|
|
30
|
+
|
|
31
|
+
async def run_agent():
|
|
32
|
+
uri = f"wss://{GATEWAY_HOST}/ws/tunnel?user={USER}&token={TOKEN}"
|
|
33
|
+
logger.info(f"Connecting to Gateway {uri}...")
|
|
34
|
+
while True:
|
|
35
|
+
try:
|
|
36
|
+
async with websockets.connect(uri, ping_interval=20, ping_timeout=20) as ws:
|
|
37
|
+
logger.info(f"Connected to Mesh Gateway as '{USER}'!")
|
|
38
|
+
async for raw_msg in ws:
|
|
39
|
+
try:
|
|
40
|
+
data = json.loads(raw_msg)
|
|
41
|
+
except Exception:
|
|
42
|
+
continue
|
|
43
|
+
req_id = data.get("id")
|
|
44
|
+
method = data.get("method")
|
|
45
|
+
params = data.get("params", {})
|
|
46
|
+
if method == "tools/call":
|
|
47
|
+
tool_name = params.get("name")
|
|
48
|
+
tool_args = params.get("arguments", {})
|
|
49
|
+
res = await handle_tool_call(tool_name, tool_args)
|
|
50
|
+
resp = {"id": req_id, "result": res}
|
|
51
|
+
await ws.send(json.dumps(resp))
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.warning(f"Connection lost: {e}. Reconnecting in 5s...")
|
|
54
|
+
await asyncio.sleep(5)
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
asyncio.run(run_agent())
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
from google.antigravity import LocalAgentConfig, types
|
|
5
|
+
from core.hooks import get_mesh_hooks
|
|
6
|
+
from core.triggers import create_mesh_watchdog_trigger, create_config_trigger
|
|
7
|
+
from core.policies import get_mesh_policies, ALLOWED_WORKSPACES
|
|
8
|
+
from core.subagents import get_mesh_subagents, get_root_capabilities_config
|
|
9
|
+
|
|
10
|
+
DEFAULT_SAVE_DIR = "/root/agy-gdrive-runner/.state/sessions"
|
|
11
|
+
DEFAULT_APP_DATA_DIR = "/root/agy-gdrive-runner/.state/artifacts"
|
|
12
|
+
|
|
13
|
+
ROOT_SYSTEM_INSTRUCTIONS = (
|
|
14
|
+
"You are the Root Orchestrator for the Antigravity Remote Execution Mesh. "
|
|
15
|
+
"Execute commands strictly through native tools and verified subagents. "
|
|
16
|
+
"Adhere to Zero Drive Queue Policy and Hard Error Halt invariants."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def create_agent_config(
|
|
20
|
+
conversation_id: Optional[str] = None,
|
|
21
|
+
save_dir: Optional[str] = None,
|
|
22
|
+
app_data_dir: Optional[str] = None,
|
|
23
|
+
interactive: bool = True,
|
|
24
|
+
max_model_calls: int = 30,
|
|
25
|
+
) -> LocalAgentConfig:
|
|
26
|
+
resolved_save_dir = save_dir or DEFAULT_SAVE_DIR
|
|
27
|
+
resolved_app_data_dir = app_data_dir or DEFAULT_APP_DATA_DIR
|
|
28
|
+
resolved_conv_id = conversation_id or str(uuid.uuid4())
|
|
29
|
+
|
|
30
|
+
os.makedirs(resolved_save_dir, exist_ok=True)
|
|
31
|
+
os.makedirs(resolved_app_data_dir, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
hooks = get_mesh_hooks()
|
|
34
|
+
triggers = [
|
|
35
|
+
create_mesh_watchdog_trigger(interval_seconds=60.0),
|
|
36
|
+
create_config_trigger(path="server_facts.json"),
|
|
37
|
+
]
|
|
38
|
+
policies = get_mesh_policies()
|
|
39
|
+
subagents = get_mesh_subagents()
|
|
40
|
+
capabilities = get_root_capabilities_config()
|
|
41
|
+
|
|
42
|
+
budget = types.BudgetConfig(
|
|
43
|
+
max_model_calls=max_model_calls,
|
|
44
|
+
max_tool_calls=60,
|
|
45
|
+
max_total_tokens=200_000,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return LocalAgentConfig(
|
|
49
|
+
system_instructions=ROOT_SYSTEM_INSTRUCTIONS,
|
|
50
|
+
conversation_id=resolved_conv_id,
|
|
51
|
+
save_dir=resolved_save_dir,
|
|
52
|
+
app_data_dir=resolved_app_data_dir,
|
|
53
|
+
hooks=hooks,
|
|
54
|
+
triggers=triggers,
|
|
55
|
+
policies=policies,
|
|
56
|
+
subagents=subagents,
|
|
57
|
+
capabilities=capabilities,
|
|
58
|
+
workspaces=ALLOWED_WORKSPACES,
|
|
59
|
+
budget_config=budget,
|
|
60
|
+
)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
from typing import List, Dict, Optional, Any
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
class CodeSymbol(BaseModel):
|
|
8
|
+
name: str
|
|
9
|
+
kind: str # "class", "function", "async_function"
|
|
10
|
+
file_path: str
|
|
11
|
+
line_start: int
|
|
12
|
+
line_end: int
|
|
13
|
+
docstring: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
class FileEntry(BaseModel):
|
|
16
|
+
path: str
|
|
17
|
+
mtime: float
|
|
18
|
+
size: int
|
|
19
|
+
symbols: List[CodeSymbol] = Field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
class CodeIndex:
|
|
22
|
+
IGNORE_DIRS = {
|
|
23
|
+
".git",
|
|
24
|
+
"__pycache__",
|
|
25
|
+
".pytest_cache",
|
|
26
|
+
".state",
|
|
27
|
+
"live_logs",
|
|
28
|
+
"node_modules",
|
|
29
|
+
".gemini",
|
|
30
|
+
".agents",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def __init__(self, root_dir: str = ".", cache_file: str = ".state/code_index.json"):
|
|
34
|
+
self.root_dir = root_dir
|
|
35
|
+
self.cache_file = cache_file
|
|
36
|
+
self.files: Dict[str, FileEntry] = {}
|
|
37
|
+
self.symbol_map: Dict[str, List[CodeSymbol]] = {}
|
|
38
|
+
self._content_cache: Dict[str, tuple[float, List[str]]] = {}
|
|
39
|
+
self._load_cache()
|
|
40
|
+
|
|
41
|
+
def _load_cache(self) -> None:
|
|
42
|
+
if os.path.exists(self.cache_file):
|
|
43
|
+
try:
|
|
44
|
+
with open(self.cache_file, "r", encoding="utf-8") as f:
|
|
45
|
+
data = json.load(f)
|
|
46
|
+
for path, entry_data in data.get("files", {}).items():
|
|
47
|
+
entry = FileEntry.model_validate(entry_data)
|
|
48
|
+
self.files[path] = entry
|
|
49
|
+
for sym in entry.symbols:
|
|
50
|
+
self.symbol_map.setdefault(sym.name, []).append(sym)
|
|
51
|
+
except Exception:
|
|
52
|
+
self.files = {}
|
|
53
|
+
self.symbol_map = {}
|
|
54
|
+
|
|
55
|
+
def _save_cache(self) -> None:
|
|
56
|
+
try:
|
|
57
|
+
os.makedirs(os.path.dirname(os.path.abspath(self.cache_file)), exist_ok=True)
|
|
58
|
+
data = {
|
|
59
|
+
"files": {k: v.model_dump() for k, v in self.files.items()}
|
|
60
|
+
}
|
|
61
|
+
tmp_file = self.cache_file + ".tmp"
|
|
62
|
+
with open(tmp_file, "w", encoding="utf-8") as f:
|
|
63
|
+
json.dump(data, f, indent=2)
|
|
64
|
+
os.replace(tmp_file, self.cache_file)
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def _parse_py_file(self, file_path: str) -> List[CodeSymbol]:
|
|
69
|
+
symbols: List[CodeSymbol] = []
|
|
70
|
+
try:
|
|
71
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
72
|
+
code = f.read()
|
|
73
|
+
tree = ast.parse(code, filename=file_path)
|
|
74
|
+
for node in ast.iter_child_nodes(tree):
|
|
75
|
+
if isinstance(node, ast.ClassDef):
|
|
76
|
+
doc = ast.get_docstring(node)
|
|
77
|
+
end_lineno = getattr(node, "end_lineno", node.lineno)
|
|
78
|
+
symbols.append(CodeSymbol(
|
|
79
|
+
name=node.name,
|
|
80
|
+
kind="class",
|
|
81
|
+
file_path=file_path,
|
|
82
|
+
line_start=node.lineno,
|
|
83
|
+
line_end=end_lineno,
|
|
84
|
+
docstring=doc,
|
|
85
|
+
))
|
|
86
|
+
for item in node.body:
|
|
87
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
88
|
+
m_doc = ast.get_docstring(item)
|
|
89
|
+
m_end = getattr(item, "end_lineno", item.lineno)
|
|
90
|
+
symbols.append(CodeSymbol(
|
|
91
|
+
name=f"{node.name}.{item.name}",
|
|
92
|
+
kind="method",
|
|
93
|
+
file_path=file_path,
|
|
94
|
+
line_start=item.lineno,
|
|
95
|
+
line_end=m_end,
|
|
96
|
+
docstring=m_doc,
|
|
97
|
+
))
|
|
98
|
+
elif isinstance(node, ast.AsyncFunctionDef):
|
|
99
|
+
doc = ast.get_docstring(node)
|
|
100
|
+
end_lineno = getattr(node, "end_lineno", node.lineno)
|
|
101
|
+
symbols.append(CodeSymbol(
|
|
102
|
+
name=node.name,
|
|
103
|
+
kind="async_function",
|
|
104
|
+
file_path=file_path,
|
|
105
|
+
line_start=node.lineno,
|
|
106
|
+
line_end=end_lineno,
|
|
107
|
+
docstring=doc,
|
|
108
|
+
))
|
|
109
|
+
elif isinstance(node, ast.FunctionDef):
|
|
110
|
+
doc = ast.get_docstring(node)
|
|
111
|
+
end_lineno = getattr(node, "end_lineno", node.lineno)
|
|
112
|
+
symbols.append(CodeSymbol(
|
|
113
|
+
name=node.name,
|
|
114
|
+
kind="function",
|
|
115
|
+
file_path=file_path,
|
|
116
|
+
line_start=node.lineno,
|
|
117
|
+
line_end=end_lineno,
|
|
118
|
+
docstring=doc,
|
|
119
|
+
))
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
return symbols
|
|
123
|
+
|
|
124
|
+
def build_or_update(self) -> Dict[str, int]:
|
|
125
|
+
total_files = 0
|
|
126
|
+
parsed_files = 0
|
|
127
|
+
cache_hits = 0
|
|
128
|
+
current_seen: set[str] = set()
|
|
129
|
+
|
|
130
|
+
for root, dirs, files in os.walk(self.root_dir):
|
|
131
|
+
dirs[:] = [d for d in dirs if d not in self.IGNORE_DIRS]
|
|
132
|
+
for file in files:
|
|
133
|
+
if not file.endswith(".py"):
|
|
134
|
+
continue
|
|
135
|
+
file_path = os.path.normpath(os.path.join(root, file))
|
|
136
|
+
current_seen.add(file_path)
|
|
137
|
+
total_files += 1
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
stat = os.stat(file_path)
|
|
141
|
+
except OSError:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
if file_path in self.files and self.files[file_path].mtime == stat.st_mtime:
|
|
145
|
+
cache_hits += 1
|
|
146
|
+
else:
|
|
147
|
+
parsed_files += 1
|
|
148
|
+
symbols = self._parse_py_file(file_path)
|
|
149
|
+
self.files[file_path] = FileEntry(
|
|
150
|
+
path=file_path,
|
|
151
|
+
mtime=stat.st_mtime,
|
|
152
|
+
size=stat.st_size,
|
|
153
|
+
symbols=symbols,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Remove deleted files
|
|
157
|
+
for old_path in list(self.files.keys()):
|
|
158
|
+
if old_path not in current_seen:
|
|
159
|
+
del self.files[old_path]
|
|
160
|
+
|
|
161
|
+
# Rebuild symbol map
|
|
162
|
+
self.symbol_map.clear()
|
|
163
|
+
for entry in self.files.values():
|
|
164
|
+
for sym in entry.symbols:
|
|
165
|
+
self.symbol_map.setdefault(sym.name, []).append(sym)
|
|
166
|
+
|
|
167
|
+
self._save_cache()
|
|
168
|
+
return {
|
|
169
|
+
"total_files": total_files,
|
|
170
|
+
"parsed_files": parsed_files,
|
|
171
|
+
"cache_hits": cache_hits,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
def find_symbol(self, name: str, exact: bool = True) -> List[CodeSymbol]:
|
|
175
|
+
if exact:
|
|
176
|
+
return self.symbol_map.get(name, [])
|
|
177
|
+
matches: List[CodeSymbol] = []
|
|
178
|
+
name_lower = name.lower()
|
|
179
|
+
for sym_name, sym_list in self.symbol_map.items():
|
|
180
|
+
if name_lower in sym_name.lower():
|
|
181
|
+
matches.extend(sym_list)
|
|
182
|
+
return matches
|
|
183
|
+
|
|
184
|
+
def get_file_outline(self, file_path: str) -> List[CodeSymbol]:
|
|
185
|
+
norm = os.path.normpath(file_path)
|
|
186
|
+
for path, entry in self.files.items():
|
|
187
|
+
if norm == path or norm.endswith(path) or path.endswith(norm):
|
|
188
|
+
return entry.symbols
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
def get_cached_chunk(self, file_path: str, start_line: int = 1, end_line: int = 100) -> str:
|
|
192
|
+
norm = os.path.normpath(file_path)
|
|
193
|
+
try:
|
|
194
|
+
stat = os.stat(norm)
|
|
195
|
+
mtime = stat.st_mtime
|
|
196
|
+
except OSError:
|
|
197
|
+
return f"Error: File '{file_path}' not found."
|
|
198
|
+
|
|
199
|
+
cached = self._content_cache.get(norm)
|
|
200
|
+
if cached is None or cached[0] != mtime:
|
|
201
|
+
try:
|
|
202
|
+
with open(norm, "r", encoding="utf-8", errors="replace") as f:
|
|
203
|
+
lines = f.readlines()
|
|
204
|
+
self._content_cache[norm] = (mtime, lines)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
return f"Error reading file '{file_path}': {e}"
|
|
207
|
+
else:
|
|
208
|
+
lines = cached[1]
|
|
209
|
+
|
|
210
|
+
total = len(lines)
|
|
211
|
+
s = max(1, start_line) - 1
|
|
212
|
+
e = min(total, end_line)
|
|
213
|
+
slice_lines = lines[s:e]
|
|
214
|
+
return f"[{norm} | Lines {s+1}-{e} of {total}]\n" + "".join(slice_lines)
|
|
215
|
+
|
|
216
|
+
def get_sitemap(self) -> Dict[str, Any]:
|
|
217
|
+
sitemap = {}
|
|
218
|
+
for path, entry in sorted(self.files.items()):
|
|
219
|
+
sitemap[path] = [
|
|
220
|
+
{"name": s.name, "kind": s.kind, "lines": f"{s.line_start}-{s.line_end}"}
|
|
221
|
+
for s in entry.symbols
|
|
222
|
+
]
|
|
223
|
+
return sitemap
|
|
224
|
+
|
|
225
|
+
_GLOBAL_INDEX: Optional[CodeIndex] = None
|
|
226
|
+
|
|
227
|
+
def get_code_index(root_dir: str = ".") -> CodeIndex:
|
|
228
|
+
global _GLOBAL_INDEX
|
|
229
|
+
if _GLOBAL_INDEX is None:
|
|
230
|
+
_GLOBAL_INDEX = CodeIndex(root_dir=root_dir)
|
|
231
|
+
_GLOBAL_INDEX.build_or_update()
|
|
232
|
+
return _GLOBAL_INDEX
|
package/core/hooks.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from google.antigravity import types
|
|
7
|
+
from google.antigravity.hooks import hooks
|
|
8
|
+
|
|
9
|
+
AUDIT_LOG_PATH = "live_logs/mcp_audit.jsonl"
|
|
10
|
+
|
|
11
|
+
DANGEROUS_PATTERNS = [
|
|
12
|
+
re.compile(r"rm\s+-rf\s+/\s*($|\*)"),
|
|
13
|
+
re.compile(r"mkfs"),
|
|
14
|
+
re.compile(r"dd\s+if=/dev/zero\s+of=/dev/sd"),
|
|
15
|
+
re.compile(r":\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:"),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
@hooks.pre_tool_call_decide
|
|
19
|
+
async def mesh_pre_tool_call(data: types.ToolCall) -> types.HookResult:
|
|
20
|
+
args = data.args or {}
|
|
21
|
+
args_str = json.dumps(args)
|
|
22
|
+
|
|
23
|
+
# 1. Block Google Drive Task Queues
|
|
24
|
+
if "antigravity_tasks.json" in args_str or "antigravity_queue" in args_str:
|
|
25
|
+
return types.HookResult(
|
|
26
|
+
allow=False,
|
|
27
|
+
message="Zero Drive Queue Policy Violation: Task queues on Google Drive are permanently disabled."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# 2. Block dangerous destructive commands
|
|
31
|
+
for key, val in args.items():
|
|
32
|
+
if isinstance(val, str):
|
|
33
|
+
for pattern in DANGEROUS_PATTERNS:
|
|
34
|
+
if pattern.search(val):
|
|
35
|
+
return types.HookResult(
|
|
36
|
+
allow=False,
|
|
37
|
+
message=f"Dangerous command blocked: matched pattern '{pattern.pattern}' in argument '{key}'"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return types.HookResult(allow=True)
|
|
41
|
+
|
|
42
|
+
@hooks.on_tool_error
|
|
43
|
+
async def mesh_on_tool_error(data: Exception) -> Optional[str]:
|
|
44
|
+
return f"[HARD ERROR HALT] Tool execution failed: {data}. Execution halted. Report to user immediately and wait for explicit instructions."
|
|
45
|
+
|
|
46
|
+
@hooks.post_tool_call
|
|
47
|
+
async def mesh_post_tool_call(data: types.ToolResult) -> None:
|
|
48
|
+
os.makedirs(os.path.dirname(AUDIT_LOG_PATH), exist_ok=True)
|
|
49
|
+
entry = {
|
|
50
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
51
|
+
"name": str(data.name),
|
|
52
|
+
"id": data.id,
|
|
53
|
+
"status": "error" if (data.error or data.exception) else "success",
|
|
54
|
+
"error": str(data.error) if data.error else (str(data.exception) if data.exception else None),
|
|
55
|
+
"server_name": data.server_name,
|
|
56
|
+
}
|
|
57
|
+
with open(AUDIT_LOG_PATH, "a", encoding="utf-8") as f:
|
|
58
|
+
f.write(json.dumps(entry) + "\n")
|
|
59
|
+
|
|
60
|
+
def get_mesh_hooks():
|
|
61
|
+
return [mesh_pre_tool_call, mesh_on_tool_error, mesh_post_tool_call]
|