gemini-computer-use 0.1.2 → 0.2.3
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/README.md +113 -2
- package/core/agent.py +69 -20
- package/core/mcp_tools.py +1658 -0
- package/core/policies.py +5 -2
- package/core/server.py +862 -5
- package/gateway.py +1491 -0
- package/install.ps1 +116 -36
- package/install.sh +116 -28
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -70,13 +70,38 @@ cd Computer-use-for-Gemini-App-Web
|
|
|
70
70
|
2. Click **Add App** (or go to **Settings ⚙️ ➔ Tools / Extensions (MCP)**).
|
|
71
71
|
3. The MCP SSE URL is **automatically copied to your clipboard** during install — just paste it (**Ctrl+V**):
|
|
72
72
|
```text
|
|
73
|
-
https
|
|
73
|
+
https://smart-server.online/sse?user=<your-node-name>&token=<your_secret_token>
|
|
74
74
|
```
|
|
75
|
+
Every node uses the **same shared domain** and is selected by `?user=` — there are no per-device subdomains to configure, and nothing extra to add to the TLS certificate.
|
|
75
76
|
4. Done! You can now prompt Gemini:
|
|
76
77
|
- *"Check system resources using system_vitals"*
|
|
77
78
|
- *"Create a FastAPI application and run test coverage via bash_exec"*
|
|
78
79
|
- *"Git commit and push changes"*
|
|
79
80
|
|
|
81
|
+
### One shared domain (canonical URL contract)
|
|
82
|
+
|
|
83
|
+
A node is **never** addressed by its own hostname. The gateway publishes a single
|
|
84
|
+
shared domain and the node name travels as a query parameter:
|
|
85
|
+
|
|
86
|
+
| Purpose | Canonical URL |
|
|
87
|
+
| :--- | :--- |
|
|
88
|
+
| MCP over SSE | `https://<shared-domain>/sse?user=<node-name>&token=<token>` |
|
|
89
|
+
| MCP Streamable HTTP | `https://<shared-domain>/mcp?user=<node-name>&token=<token>` |
|
|
90
|
+
| Reverse tunnel (agent) | `wss://<shared-domain>/ws/tunnel?user=<node-name>&token=<token>` |
|
|
91
|
+
|
|
92
|
+
Why: every extra hostname would need its own DNS record *and* its own SAN in the
|
|
93
|
+
TLS certificate. A node whose name is missing from the certificate fails the TLS
|
|
94
|
+
handshake, and the Gemini client then reports an opaque "cannot connect to host".
|
|
95
|
+
With one shared domain the certificate covers every node forever, and adding a
|
|
96
|
+
node is only a registration call.
|
|
97
|
+
|
|
98
|
+
- The shared domain defaults to `smart-server.online`; override it with
|
|
99
|
+
`./install.sh --domain=<shared-domain>` (node side) or `MESH_PUBLIC_URL`
|
|
100
|
+
(gateway side).
|
|
101
|
+
- Legacy per-device subdomain URLs still resolve for backwards compatibility and
|
|
102
|
+
log a deprecation warning; set `MESH_LEGACY_SUBDOMAIN=0` on the gateway to
|
|
103
|
+
reject them outright.
|
|
104
|
+
|
|
80
105
|
---
|
|
81
106
|
|
|
82
107
|
## 🖥️ Deployment Modes
|
|
@@ -101,6 +126,41 @@ Automatically transfers files and configures the node on your remote server via
|
|
|
101
126
|
|
|
102
127
|
---
|
|
103
128
|
|
|
129
|
+
## 🚢 Gateway Operations
|
|
130
|
+
|
|
131
|
+
The gateway is a single host serving every node. Publish it only through the
|
|
132
|
+
deploy script, so the running gateway, the served installers and the node
|
|
133
|
+
bootstrap payload cannot drift apart:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
MESH_GATEWAY_SSH=root@<gateway-ip> ./deploy_gateway.sh # deploy
|
|
137
|
+
MESH_GATEWAY_SSH=root@<gateway-ip> ./deploy_gateway.sh --dry-run # preview
|
|
138
|
+
MESH_GATEWAY_SSH=root@<gateway-ip> MESH_GATEWAY_SSH_PASS_FILE=~/.ssh/gw-pass \
|
|
139
|
+
./deploy_gateway.sh # password auth
|
|
140
|
+
```
|
|
141
|
+
It verifies every upload with an sha256 manifest on the host, takes a
|
|
142
|
+
timestamped backup, installs with the right owner, restarts the service and
|
|
143
|
+
finishes with a public health check. Nothing is installed if verification fails.
|
|
144
|
+
|
|
145
|
+
### TLS certificate policy (one name, no per-device SANs)
|
|
146
|
+
|
|
147
|
+
The certificate must cover the **shared domain only**. No device name is ever
|
|
148
|
+
added to it, because a node is selected by `?user=` and never by a hostname:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# what the certificate currently covers
|
|
152
|
+
sudo certbot certificates | grep -A1 'Certificate Name: smart-server.online'
|
|
153
|
+
|
|
154
|
+
# canonical renewal check (staging, does not touch the live certificate)
|
|
155
|
+
sudo certbot renew --dry-run --cert-name smart-server.online
|
|
156
|
+
```
|
|
157
|
+
Legacy per-device SANs may still be present from before this contract. They are
|
|
158
|
+
harmless and keep old bookmarked URLs working; drop them from the lineage at the
|
|
159
|
+
next renewal once no client uses subdomain URLs any more (see the `domains =`
|
|
160
|
+
line in `/etc/letsencrypt/renewal/<name>.conf`, or re-issue with a single `-d`).
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
104
164
|
## 🔒 Security Architecture
|
|
105
165
|
|
|
106
166
|
- **Token-Gated Endpoints**: Every incoming request must contain the cryptographic token via `?token=...` or `Authorization: Bearer`.
|
|
@@ -185,10 +245,61 @@ cd Computer-use-for-Gemini-App-Web
|
|
|
185
245
|
2. Нажмите **Добавить приложение** (Add App / Настройки ➔ MCP).
|
|
186
246
|
3. Ссылка на ваш MCP-сервер **автоматически скопирована в буфер обмена** при установке — просто вставьте её (**Ctrl+V**):
|
|
187
247
|
```text
|
|
188
|
-
https
|
|
248
|
+
https://smart-server.online/sse?user=<имя-вашего-узла>&token=<ваш_секретный_токен>
|
|
189
249
|
```
|
|
250
|
+
Все узлы используют **один общий домен**, а нужный узел выбирается параметром `?user=` — субдомены для каждого устройства не создаются, и добавлять что-либо в TLS-сертификат не нужно.
|
|
190
251
|
4. Готово! Теперь Gemini может выполнять команды прямо на вашей машине.
|
|
191
252
|
|
|
253
|
+
### Один общий домен (канонический формат URL)
|
|
254
|
+
|
|
255
|
+
Узел **никогда** не адресуется собственным именем хоста. Шлюз публикует один
|
|
256
|
+
общий домен, а имя узла передаётся параметром:
|
|
257
|
+
|
|
258
|
+
| Назначение | Канонический URL |
|
|
259
|
+
| :--- | :--- |
|
|
260
|
+
| MCP через SSE | `https://<общий-домен>/sse?user=<имя-узла>&token=<токен>` |
|
|
261
|
+
| MCP Streamable HTTP | `https://<общий-домен>/mcp?user=<имя-узла>&token=<токен>` |
|
|
262
|
+
| Обратный туннель (агент) | `wss://<общий-домен>/ws/tunnel?user=<имя-узла>&token=<токен>` |
|
|
263
|
+
|
|
264
|
+
Почему так: каждому дополнительному имени хоста нужна своя DNS-запись **и** свой
|
|
265
|
+
SAN в TLS-сертификате. Если имени нет в сертификате, TLS-рукопожатие падает, и
|
|
266
|
+
клиент Gemini показывает невнятную ошибку «не удаётся подключиться к хосту».
|
|
267
|
+
С одним общим доменом сертификат покрывает все узлы сразу, а добавление узла —
|
|
268
|
+
это только вызов регистрации.
|
|
269
|
+
|
|
270
|
+
- Общий домен по умолчанию `smart-server.online`; переопределяется через
|
|
271
|
+
`./install.sh --domain=<общий-домен>` (на стороне узла) или `MESH_PUBLIC_URL`
|
|
272
|
+
(на стороне шлюза).
|
|
273
|
+
- Старые ссылки с субдоменами устройства продолжают работать для совместимости
|
|
274
|
+
и пишут предупреждение в лог; `MESH_LEGACY_SUBDOMAIN=0` на шлюзе отключает их.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 🚢 Эксплуатация шлюза
|
|
279
|
+
|
|
280
|
+
Шлюз — один хост на все узлы. Публикуйте его только скриптом, чтобы работающий
|
|
281
|
+
шлюз, отдаваемые установщики и bootstrap-код узлов не расходились:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
MESH_GATEWAY_SSH=root@<ip-шлюза> ./deploy_gateway.sh # задеплоить
|
|
285
|
+
MESH_GATEWAY_SSH=root@<ip-шлюза> ./deploy_gateway.sh --dry-run # предпросмотр
|
|
286
|
+
```
|
|
287
|
+
Скрипт проверяет каждую загрузку манифестом sha256 на хосте, делает бэкап с
|
|
288
|
+
меткой времени, ставит файлы с нужным владельцем, перезапускает службу и
|
|
289
|
+
завершает публичной проверкой здоровья. При несовпадении хэшей ничего не
|
|
290
|
+
устанавливается.
|
|
291
|
+
|
|
292
|
+
**Политика TLS-сертификата: только общий домен, без SAN на устройства.**
|
|
293
|
+
Имена узлов в сертификат не добавляются никогда — узел выбирается `?user=`:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
sudo certbot certificates | grep -A1 'Certificate Name: smart-server.online'
|
|
297
|
+
sudo certbot renew --dry-run --cert-name smart-server.online # проверка продления
|
|
298
|
+
```
|
|
299
|
+
Старые SAN'ы устройств могут остаться с прежнего контракта: они безвредны и
|
|
300
|
+
поддерживают совместимость старых ссылок. Убрать их можно при следующем
|
|
301
|
+
продлении, когда субдоменными ссылками никто не пользуется.
|
|
302
|
+
|
|
192
303
|
---
|
|
193
304
|
|
|
194
305
|
## 📄 Лицензия
|
package/core/agent.py
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
|
+
"""core/agent.py - Antigravity Mesh tunnel node (thin transport).
|
|
2
|
+
|
|
3
|
+
All tool logic lives in :mod:`core.mcp_tools`; this module only maintains the
|
|
4
|
+
websocket tunnel to the gateway and forwards ``tools/call`` requests. The
|
|
5
|
+
response shape is unchanged (``stdout``/``stderr``/``exit_code`` and friends),
|
|
6
|
+
so an unmodified gateway keeps working.
|
|
7
|
+
|
|
8
|
+
Configuration (read once at import/startup)::
|
|
9
|
+
|
|
10
|
+
MESH_WORKSPACE base directory for relative paths (default: cwd)
|
|
11
|
+
MESH_READ_ONLY "1"/"true" -> mutating tools refuse to run
|
|
12
|
+
MESH_WRITE_ROOTS pathsep-separated list of roots allowed for writes
|
|
13
|
+
"""
|
|
14
|
+
|
|
1
15
|
import asyncio
|
|
2
16
|
import json
|
|
3
17
|
import logging
|
|
4
18
|
import os
|
|
5
|
-
import
|
|
19
|
+
import random
|
|
20
|
+
|
|
6
21
|
import websockets
|
|
7
|
-
|
|
22
|
+
|
|
23
|
+
from core import mcp_tools
|
|
8
24
|
|
|
9
25
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
10
26
|
logger = logging.getLogger("agy-agent")
|
|
@@ -13,28 +29,53 @@ GATEWAY_HOST = os.environ.get("MESH_GATEWAY", "smart-server.online")
|
|
|
13
29
|
USER = os.environ.get("MESH_USER", "levra7")
|
|
14
30
|
TOKEN = os.environ.get("MESH_TOKEN", "")
|
|
15
31
|
|
|
32
|
+
|
|
33
|
+
def _env_flag(name: str) -> bool:
|
|
34
|
+
return str(os.environ.get(name, "")).strip().lower() in ("1", "true", "yes", "on")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def configure_from_env() -> None:
|
|
38
|
+
"""Apply MESH_* environment configuration to the shared tool module."""
|
|
39
|
+
write_roots = os.environ.get("MESH_WRITE_ROOTS") or None
|
|
40
|
+
jobs_dir = os.environ.get("MESH_JOBS_DIR") or None
|
|
41
|
+
max_chars = os.environ.get("MESH_MAX_OUTPUT_CHARS")
|
|
42
|
+
mcp_tools.configure(
|
|
43
|
+
workspace=os.environ.get("MESH_WORKSPACE") or os.getcwd(),
|
|
44
|
+
read_only=_env_flag("MESH_READ_ONLY"),
|
|
45
|
+
write_roots=write_roots,
|
|
46
|
+
jobs_dir=jobs_dir,
|
|
47
|
+
max_output_chars=max_chars,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Configure as soon as the node module is imported so external callers (tests,
|
|
52
|
+
# harnesses) observe the environment-driven settings.
|
|
53
|
+
configure_from_env()
|
|
54
|
+
|
|
55
|
+
|
|
16
56
|
async def handle_tool_call(name: str, args: dict) -> dict:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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}"}
|
|
57
|
+
"""Forward one tool call to the shared implementation in a worker thread."""
|
|
58
|
+
logger.info("Executing %s", name)
|
|
59
|
+
return await asyncio.to_thread(mcp_tools.call_tool, name, args or {})
|
|
60
|
+
|
|
30
61
|
|
|
31
62
|
async def run_agent():
|
|
63
|
+
"""Stay connected to the gateway, healing every kind of link failure.
|
|
64
|
+
|
|
65
|
+
A dropped websocket is retried with exponential backoff plus jitter: a short
|
|
66
|
+
first retry restores a flapping link quickly, while the growing cap stops a
|
|
67
|
+
hammering loop when the network is genuinely down. The backoff resets after a
|
|
68
|
+
healthy connection, so a long-lived session is not penalised.
|
|
69
|
+
"""
|
|
32
70
|
uri = f"wss://{GATEWAY_HOST}/ws/tunnel?user={USER}&token={TOKEN}"
|
|
33
71
|
logger.info(f"Connecting to Gateway {uri}...")
|
|
72
|
+
backoff = 1.0
|
|
34
73
|
while True:
|
|
35
74
|
try:
|
|
36
|
-
async with websockets.connect(uri, ping_interval=
|
|
75
|
+
async with websockets.connect(uri, ping_interval=10, ping_timeout=10,
|
|
76
|
+
close_timeout=5) as ws:
|
|
37
77
|
logger.info(f"Connected to Mesh Gateway as '{USER}'!")
|
|
78
|
+
backoff = 1.0 # healthy again
|
|
38
79
|
async for raw_msg in ws:
|
|
39
80
|
try:
|
|
40
81
|
data = json.loads(raw_msg)
|
|
@@ -46,12 +87,20 @@ async def run_agent():
|
|
|
46
87
|
if method == "tools/call":
|
|
47
88
|
tool_name = params.get("name")
|
|
48
89
|
tool_args = params.get("arguments", {})
|
|
49
|
-
|
|
90
|
+
try:
|
|
91
|
+
res = await handle_tool_call(tool_name, tool_args)
|
|
92
|
+
except Exception as tool_exc:
|
|
93
|
+
# A tool must never take the tunnel down with it.
|
|
94
|
+
logger.warning(f"Tool {tool_name} failed: {tool_exc}")
|
|
95
|
+
res = {"error": f"{type(tool_exc).__name__}: {tool_exc}"}
|
|
50
96
|
resp = {"id": req_id, "result": res}
|
|
51
|
-
await ws.send(json.dumps(resp))
|
|
97
|
+
await ws.send(json.dumps(resp, ensure_ascii=False))
|
|
52
98
|
except Exception as e:
|
|
53
|
-
|
|
54
|
-
|
|
99
|
+
delay = min(backoff, 15.0) + random.uniform(0, 0.5)
|
|
100
|
+
logger.warning(f"Connection lost: {e}. Reconnecting in {delay:.1f}s...")
|
|
101
|
+
await asyncio.sleep(delay)
|
|
102
|
+
backoff = min(backoff * 2, 15.0)
|
|
103
|
+
|
|
55
104
|
|
|
56
105
|
if __name__ == "__main__":
|
|
57
106
|
asyncio.run(run_agent())
|