gemini-computer-use 0.2.3 β 0.2.4
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 +221 -151
- package/core/agent.py +200 -24
- package/core/mcp_tools.py +125 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,87 +1,139 @@
|
|
|
1
|
-
#
|
|
1
|
+
# π± Gemini Computer Use β control any PC from Gemini Spark (App & Web)
|
|
2
2
|
|
|
3
|
-
> **
|
|
3
|
+
> **An MCP server that turns Google Gemini from a chatbot into a full-fledged mobile agent.**
|
|
4
|
+
> Type a task in the Gemini app on your phone β your PC, laptop or server executes it, and Gemini reports back with real results.
|
|
4
5
|
|
|
5
6
|
[English](README.md) | [π·πΊ Π ΡΡΡΠΊΠ°Ρ Π²Π΅ΡΡΠΈΡ](#-ΡΡΡΡΠΊΠ°Ρ-Π²Π΅ΡΡΠΈΡ)
|
|
6
7
|
|
|
7
8
|
---
|
|
8
9
|
|
|
9
|
-
## π‘
|
|
10
|
+
## π‘ The idea
|
|
11
|
+
|
|
12
|
+
Gemini in a browser or on a phone can talk, but it cannot *do* anything on your computer.
|
|
13
|
+
**Gemini Computer Use** closes that gap. It is a free, open-source **Model Context Protocol (MCP)** server that you connect to **Gemini Spark** ([gemini.google.com/spark/apps](https://gemini.google.com/spark/apps) or the Gemini mobile app) with a single link. After that, Gemini gets hands:
|
|
14
|
+
|
|
15
|
+
- runs shell commands, builds and tests code, manages git;
|
|
16
|
+
- reads, writes and edits files;
|
|
17
|
+
- searches the file system;
|
|
18
|
+
- starts long-running jobs and checks on them later;
|
|
19
|
+
- watches CPU, RAM and disk.
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
π± Gemini app / π gemini.google.com (Spark)
|
|
23
|
+
β MCP (SSE / Streamable HTTP)
|
|
24
|
+
βΌ
|
|
25
|
+
βοΈ Gateway smart-server.online β relay only, TLS
|
|
26
|
+
β outbound WebSocket tunnel (no open ports)
|
|
27
|
+
βΌ
|
|
28
|
+
π» Your PC Β· laptop Β· VPS Β· home server β executes the task
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Result:** your phone becomes a remote control for every machine you own β and Gemini is the agent that operates them.
|
|
32
|
+
|
|
33
|
+
---
|
|
10
34
|
|
|
11
|
-
|
|
35
|
+
## π₯ What you can do with it
|
|
12
36
|
|
|
13
|
-
|
|
37
|
+
Just ask Gemini in plain language, from anywhere:
|
|
38
|
+
|
|
39
|
+
- *"Check why my home server is slow and show the top processes"*
|
|
40
|
+
- *"Pull the latest changes in ~/projects/api, run the tests and tell me what failed"*
|
|
41
|
+
- *"Fix the typo in config.yaml and restart the service"*
|
|
42
|
+
- *"Start a full backup in the background and tell me when it's done"*
|
|
43
|
+
- *"How much free disk space is left on my laptop?"*
|
|
44
|
+
|
|
45
|
+
No SSH client, no terminal on your phone, no VPN β only a chat.
|
|
14
46
|
|
|
15
47
|
---
|
|
16
48
|
|
|
17
|
-
## β¨
|
|
49
|
+
## β¨ Why it works this way
|
|
18
50
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- π» **Cross-Platform**: Fully supports Linux, macOS, and Windows.
|
|
51
|
+
| | |
|
|
52
|
+
| :--- | :--- |
|
|
53
|
+
| π± **Mobile-first agent** | Works in the official Gemini mobile app and on the web through Gemini Spark. |
|
|
54
|
+
| π₯οΈ **Any PC** | Linux, macOS, Windows β desktops, laptops, VPS, containers. |
|
|
55
|
+
| π **Behind any NAT** | The node opens an *outbound* WebSocket tunnel. No public IP, no port forwarding, no router setup. |
|
|
56
|
+
| β‘ **One-line install** | Detects OS, architecture and device type; installs autostart (`systemd` / `launchd` / Windows task); copies your connection link to the clipboard. |
|
|
57
|
+
| π‘οΈ **Token-gated** | Every call needs a personal 128-bit token (`?token=β¦` or `Authorization: Bearer`). Everything else gets HTTP 401. |
|
|
58
|
+
| π **Many machines, one gateway** | Each node is addressed by `?user=<node-name>` on one shared domain. |
|
|
59
|
+
| π **Free & open source** | MIT license, works with the free Gemini tier. |
|
|
29
60
|
|
|
30
61
|
---
|
|
31
62
|
|
|
32
|
-
## π οΈ
|
|
63
|
+
## π οΈ What Gemini can do on your machine (MCP tools)
|
|
33
64
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
65
|
+
| Category | Tools |
|
|
66
|
+
| :--- | :--- |
|
|
67
|
+
| Terminal | `bash_exec(command)` |
|
|
68
|
+
| Files | `list_dir(path)` Β· `read_file(path, start_line, end_line)` Β· `write_file(path, content)` Β· `edit_file(path, old_string, new_string)` |
|
|
69
|
+
| Search | `grep_search(pattern, path)` Β· `glob_find(pattern, path)` |
|
|
70
|
+
| Long-running jobs | `run_job(command)` β `job_output(job_id)` Β· `job_kill(job_id)` Β· `job_list()` |
|
|
71
|
+
| Status | `system_vitals()` Β· `system_info()` Β· `mesh_status()` |
|
|
72
|
+
| Agent behaviour | `get_orchestration_skill()` β loads the agent's operating rules |
|
|
73
|
+
|
|
74
|
+
> [!TIP]
|
|
75
|
+
> Gemini gives a single tool call roughly 30 seconds. For anything longer (builds, backups, downloads) the agent uses `run_job` and polls `job_output`, so tasks never get cut off.
|
|
38
76
|
|
|
39
77
|
---
|
|
40
78
|
|
|
41
|
-
## π Quick
|
|
79
|
+
## π Quick start β 2 minutes
|
|
42
80
|
|
|
43
|
-
###
|
|
81
|
+
### 1. Install the node on the PC you want to control
|
|
82
|
+
|
|
83
|
+
**π§ Linux / π macOS**
|
|
44
84
|
```bash
|
|
45
85
|
curl -fsSL https://smart-server.online/install.sh | bash
|
|
46
86
|
```
|
|
47
87
|
|
|
48
|
-
|
|
88
|
+
**πͺ Windows (PowerShell)**
|
|
49
89
|
```powershell
|
|
50
90
|
irm https://smart-server.online/install.ps1 | iex
|
|
51
91
|
```
|
|
52
92
|
|
|
53
|
-
|
|
93
|
+
**π¦ Node.js (any OS)**
|
|
54
94
|
```bash
|
|
55
95
|
npx gemini-computer-use
|
|
56
96
|
```
|
|
57
97
|
|
|
58
|
-
|
|
98
|
+
**π οΈ From source**
|
|
59
99
|
```bash
|
|
60
100
|
git clone https://github.com/LevRa7/Computer-use-for-Gemini-App-Web.git
|
|
61
101
|
cd Computer-use-for-Gemini-App-Web
|
|
62
102
|
./install.sh --quick
|
|
63
103
|
```
|
|
64
104
|
|
|
105
|
+
At the end the installer prints your personal MCP link and copies it to the clipboard:
|
|
106
|
+
```text
|
|
107
|
+
https://smart-server.online/sse?user=<your-node-name>&token=<your_secret_token>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 2. Connect it to Gemini Spark
|
|
111
|
+
|
|
112
|
+
1. Open **[gemini.google.com/spark/apps](https://gemini.google.com/spark/apps)** in a browser or in the Gemini mobile app.
|
|
113
|
+
2. Tap **Add App** (or **Settings βοΈ β Tools / Extensions (MCP)**).
|
|
114
|
+
3. Paste the link (**Ctrl+V**) and save.
|
|
115
|
+
|
|
116
|
+
### 3. Give it a task
|
|
117
|
+
|
|
118
|
+
> *"Show system_vitals and list the 5 biggest folders in my home directory."*
|
|
119
|
+
|
|
120
|
+
That's it β Gemini is now an agent running on your machine. Repeat step 1 on other computers to control all of them from the same phone.
|
|
121
|
+
|
|
65
122
|
---
|
|
66
123
|
|
|
67
|
-
##
|
|
124
|
+
## π₯οΈ Deployment modes
|
|
68
125
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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.
|
|
76
|
-
4. Done! You can now prompt Gemini:
|
|
77
|
-
- *"Check system resources using system_vitals"*
|
|
78
|
-
- *"Create a FastAPI application and run test coverage via bash_exec"*
|
|
79
|
-
- *"Git commit and push changes"*
|
|
126
|
+
| Mode | Command | When to use |
|
|
127
|
+
| :--- | :--- | :--- |
|
|
128
|
+
| **Gateway + tunnel** (recommended) | `./install.sh --quick` | Home PCs and laptops behind NAT β the mobile-agent scenario. |
|
|
129
|
+
| **Remote over SSH** | `./install.sh --ssh=user@host` | Deploy a node onto a remote Linux server from your terminal. |
|
|
130
|
+
| **Local standalone** | `./install.sh --mode=standalone --port=8096` | Pure localhost FastMCP server at `http://localhost:8096/sse`, no cloud relay. |
|
|
80
131
|
|
|
81
|
-
|
|
132
|
+
---
|
|
82
133
|
|
|
83
|
-
|
|
84
|
-
|
|
134
|
+
## π One shared domain (canonical URL contract)
|
|
135
|
+
|
|
136
|
+
A node is **never** addressed by its own hostname. The gateway publishes a single shared domain, and the node name travels as a query parameter:
|
|
85
137
|
|
|
86
138
|
| Purpose | Canonical URL |
|
|
87
139
|
| :--- | :--- |
|
|
@@ -89,48 +141,28 @@ shared domain and the node name travels as a query parameter:
|
|
|
89
141
|
| MCP Streamable HTTP | `https://<shared-domain>/mcp?user=<node-name>&token=<token>` |
|
|
90
142
|
| Reverse tunnel (agent) | `wss://<shared-domain>/ws/tunnel?user=<node-name>&token=<token>` |
|
|
91
143
|
|
|
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.
|
|
144
|
+
Why: every extra hostname would need its own DNS record *and* its own SAN in the TLS certificate. A node whose name is missing from the certificate fails the TLS handshake, and the Gemini client then reports an opaque "cannot connect to host". With one shared domain the certificate covers every node forever, and adding a node is only a registration call.
|
|
97
145
|
|
|
98
|
-
- The shared domain defaults to `smart-server.online`; override it with
|
|
99
|
-
|
|
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.
|
|
146
|
+
- The shared domain defaults to `smart-server.online`; override it with `./install.sh --domain=<shared-domain>` (node side) or `MESH_PUBLIC_URL` (gateway side).
|
|
147
|
+
- Legacy per-device subdomain URLs still resolve for backwards compatibility and log a deprecation warning; set `MESH_LEGACY_SUBDOMAIN=0` on the gateway to reject them outright.
|
|
104
148
|
|
|
105
149
|
---
|
|
106
150
|
|
|
107
|
-
##
|
|
151
|
+
## π Security
|
|
108
152
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
Perfect for personal laptops and home workstations behind NAT. The connection is maintained via an outbound secure WebSocket tunnel.
|
|
114
|
-
|
|
115
|
-
### Mode 2: Local Standalone
|
|
116
|
-
```bash
|
|
117
|
-
./install.sh --mode=standalone --port=8096
|
|
118
|
-
```
|
|
119
|
-
Runs an isolated, local FastMCP server on `http://localhost:8096/sse` without cloud relaying.
|
|
153
|
+
- **Token-gated endpoints** β every request must carry the node's secret token.
|
|
154
|
+
- **Outbound-only tunnel** β nodes never listen on public ports; the agent dials out to the gateway.
|
|
155
|
+
- **Gateway is a relay** β it terminates TLS and forwards calls; commands run only on your node.
|
|
156
|
+
- **Unprivileged by default** β the agent runs in user space, without root.
|
|
120
157
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
./install.sh --ssh=user@my-remote-server.com
|
|
124
|
-
```
|
|
125
|
-
Automatically transfers files and configures the node on your remote server via SSH.
|
|
158
|
+
> [!WARNING]
|
|
159
|
+
> Whoever has your MCP link can run commands on that machine. Treat it like a password: don't publish it, and reinstall the node to rotate the token if it leaks.
|
|
126
160
|
|
|
127
161
|
---
|
|
128
162
|
|
|
129
|
-
## π’
|
|
163
|
+
## π’ Self-hosting the gateway
|
|
130
164
|
|
|
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:
|
|
165
|
+
The gateway is a single host serving every node. Publish it only through the deploy script, so the running gateway, the served installers and the node bootstrap payload cannot drift apart:
|
|
134
166
|
|
|
135
167
|
```bash
|
|
136
168
|
MESH_GATEWAY_SSH=root@<gateway-ip> ./deploy_gateway.sh # deploy
|
|
@@ -138,14 +170,9 @@ MESH_GATEWAY_SSH=root@<gateway-ip> ./deploy_gateway.sh --dry-run # preview
|
|
|
138
170
|
MESH_GATEWAY_SSH=root@<gateway-ip> MESH_GATEWAY_SSH_PASS_FILE=~/.ssh/gw-pass \
|
|
139
171
|
./deploy_gateway.sh # password auth
|
|
140
172
|
```
|
|
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.
|
|
173
|
+
It verifies every upload with an sha256 manifest on the host, takes a timestamped backup, installs with the right owner, restarts the service and finishes with a public health check. Nothing is installed if verification fails.
|
|
144
174
|
|
|
145
|
-
|
|
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:
|
|
175
|
+
**TLS certificate policy β one name, no per-device SANs.** The certificate must cover the shared domain only; a node is selected by `?user=`, never by a hostname:
|
|
149
176
|
|
|
150
177
|
```bash
|
|
151
178
|
# what the certificate currently covers
|
|
@@ -154,106 +181,151 @@ sudo certbot certificates | grep -A1 'Certificate Name: smart-server.online'
|
|
|
154
181
|
# canonical renewal check (staging, does not touch the live certificate)
|
|
155
182
|
sudo certbot renew --dry-run --cert-name smart-server.online
|
|
156
183
|
```
|
|
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`).
|
|
184
|
+
Legacy per-device SANs may still be present from before this contract. They are harmless and keep old bookmarked URLs working; drop them at the next renewal once no client uses subdomain URLs any more (see the `domains =` line in `/etc/letsencrypt/renewal/<name>.conf`, or re-issue with a single `-d`).
|
|
161
185
|
|
|
162
186
|
---
|
|
163
187
|
|
|
164
|
-
##
|
|
188
|
+
## π License
|
|
165
189
|
|
|
166
|
-
|
|
167
|
-
- **Outbound-Only Tunnel**: Local nodes do not open listening public ports. The agent initiates an outbound WebSocket connection to the gateway.
|
|
168
|
-
- **Unprivileged Execution**: Agents execute in user-space by default without requiring root privileges.
|
|
190
|
+
**MIT** β see [LICENSE](LICENSE).
|
|
169
191
|
|
|
192
|
+
---
|
|
170
193
|
---
|
|
171
194
|
|
|
172
|
-
|
|
195
|
+
# π·πΊ Π ΡΡΡΠΊΠ°Ρ Π²Π΅ΡΡΠΈΡ
|
|
173
196
|
|
|
174
|
-
|
|
197
|
+
> **MCP-ΡΠ΅ΡΠ²Π΅Ρ, ΠΊΠΎΡΠΎΡΡΠΉ ΠΏΡΠ΅Π²ΡΠ°ΡΠ°Π΅Ρ Google Gemini ΠΈΠ· ΡΠ°Ρ-Π±ΠΎΡΠ° Π² ΠΏΠΎΠ»Π½ΠΎΡΠ΅Π½Π½ΠΎΠ³ΠΎ ΠΌΠΎΠ±ΠΈΠ»ΡΠ½ΠΎΠ³ΠΎ Π°Π³Π΅Π½ΡΠ°.**
|
|
198
|
+
> ΠΠ°ΠΏΠΈΡΠΈΡΠ΅ Π·Π°Π΄Π°ΡΡ Π² ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ Gemini Π½Π° ΡΠ΅Π»Π΅ΡΠΎΠ½Π΅ β Π²Π°Ρ ΠΠ, Π½ΠΎΡΡΠ±ΡΠΊ ΠΈΠ»ΠΈ ΡΠ΅ΡΠ²Π΅Ρ Π²ΡΠΏΠΎΠ»Π½ΠΈΡ Π΅Ρ, Π° Gemini Π²Π΅ΡΠ½ΡΡΡΡ Ρ ΡΠ΅Π°Π»ΡΠ½ΡΠΌ ΡΠ΅Π·ΡΠ»ΡΡΠ°ΡΠΎΠΌ.
|
|
175
199
|
|
|
176
|
-
---
|
|
177
200
|
---
|
|
178
201
|
|
|
179
|
-
|
|
202
|
+
## π‘ ΠΠ΄Π΅Ρ
|
|
203
|
+
|
|
204
|
+
Gemini Π² Π±ΡΠ°ΡΠ·Π΅ΡΠ΅ ΠΈΠ»ΠΈ Π½Π° ΡΠ΅Π»Π΅ΡΠΎΠ½Π΅ ΡΠΌΠ΅Π΅Ρ ΡΠ°Π·Π³ΠΎΠ²Π°ΡΠΈΠ²Π°ΡΡ, Π½ΠΎ Π½ΠΈΡΠ΅Π³ΠΎ Π½Π΅ ΠΌΠΎΠΆΠ΅Ρ *ΡΠ΄Π΅Π»Π°ΡΡ* Π½Π° Π²Π°ΡΠ΅ΠΌ ΠΊΠΎΠΌΠΏΡΡΡΠ΅ΡΠ΅.
|
|
205
|
+
**Gemini Computer Use** ΡΡΡΡΠ°Π½ΡΠ΅Ρ ΡΡΠΎΡ ΡΠ°Π·ΡΡΠ². ΠΡΠΎ Π±Π΅ΡΠΏΠ»Π°ΡΠ½ΡΠΉ open-source ΡΠ΅ΡΠ²Π΅Ρ **Model Context Protocol (MCP)**, ΠΊΠΎΡΠΎΡΡΠΉ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ°Π΅ΡΡΡ ΠΊ **Gemini Spark** ([gemini.google.com/spark/apps](https://gemini.google.com/spark/apps) ΠΈΠ»ΠΈ ΠΌΠΎΠ±ΠΈΠ»ΡΠ½ΠΎΠ΅ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Gemini) ΠΎΠ΄Π½ΠΎΠΉ ΡΡΡΠ»ΠΊΠΎΠΉ. ΠΠΎΡΠ»Π΅ ΡΡΠΎΠ³ΠΎ Ρ Gemini ΠΏΠΎΡΠ²Π»ΡΡΡΡΡ Β«ΡΡΠΊΠΈΒ»:
|
|
206
|
+
|
|
207
|
+
- Π²ΡΠΏΠΎΠ»Π½ΡΠ΅Ρ ΠΊΠΎΠΌΠ°Π½Π΄Ρ ΡΠ΅ΡΠΌΠΈΠ½Π°Π»Π°, ΡΠΎΠ±ΠΈΡΠ°Π΅Ρ ΠΈ ΡΠ΅ΡΡΠΈΡΡΠ΅Ρ ΠΊΠΎΠ΄, ΡΠ°Π±ΠΎΡΠ°Π΅Ρ Ρ git;
|
|
208
|
+
- ΡΠΈΡΠ°Π΅Ρ, ΡΠΎΠ·Π΄Π°ΡΡ ΠΈ ΡΠ΅Π΄Π°ΠΊΡΠΈΡΡΠ΅Ρ ΡΠ°ΠΉΠ»Ρ;
|
|
209
|
+
- ΠΈΡΠ΅Ρ ΠΏΠΎ ΡΠ°ΠΉΠ»ΠΎΠ²ΠΎΠΉ ΡΠΈΡΡΠ΅ΠΌΠ΅;
|
|
210
|
+
- Π·Π°ΠΏΡΡΠΊΠ°Π΅Ρ Π΄ΠΎΠ»Π³ΠΈΠ΅ Π·Π°Π΄Π°ΡΠΈ Π² ΡΠΎΠ½Π΅ ΠΈ ΠΏΡΠΎΠ²Π΅ΡΡΠ΅Ρ ΠΈΡ
ΠΏΠΎΠ·ΠΆΠ΅;
|
|
211
|
+
- ΡΠ»Π΅Π΄ΠΈΡ Π·Π° CPU, RAM ΠΈ Π΄ΠΈΡΠΊΠΎΠΌ.
|
|
212
|
+
|
|
213
|
+
```text
|
|
214
|
+
π± ΠΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Gemini / π gemini.google.com (Spark)
|
|
215
|
+
β MCP (SSE / Streamable HTTP)
|
|
216
|
+
βΌ
|
|
217
|
+
βοΈ Π¨Π»ΡΠ· smart-server.online β ΡΠΎΠ»ΡΠΊΠΎ ΡΠ΅Π»Π΅ΠΉ, TLS
|
|
218
|
+
β ΠΈΡΡ
ΠΎΠ΄ΡΡΠΈΠΉ WebSocket-ΡΡΠ½Π½Π΅Π»Ρ (Π±Π΅Π· ΠΎΡΠΊΡΡΡΡΡ
ΠΏΠΎΡΡΠΎΠ²)
|
|
219
|
+
βΌ
|
|
220
|
+
π» ΠΠ°Ρ ΠΠ Β· Π½ΠΎΡΡΠ±ΡΠΊ Β· VPS Β· Π΄ΠΎΠΌΠ°ΡΠ½ΠΈΠΉ ΡΠ΅ΡΠ²Π΅Ρ β Π²ΡΠΏΠΎΠ»Π½ΡΠ΅Ρ Π·Π°Π΄Π°ΡΡ
|
|
221
|
+
```
|
|
180
222
|
|
|
181
|
-
|
|
223
|
+
**ΠΡΠΎΠ³:** ΡΠ΅Π»Π΅ΡΠΎΠ½ ΡΡΠ°Π½ΠΎΠ²ΠΈΡΡΡ ΠΏΡΠ»ΡΡΠΎΠΌ ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ Π²ΡΠ΅ΠΌΠΈ Π²Π°ΡΠΈΠΌΠΈ ΠΌΠ°ΡΠΈΠ½Π°ΠΌΠΈ, Π° Gemini β Π°Π³Π΅Π½ΡΠΎΠΌ, ΠΊΠΎΡΠΎΡΡΠΉ ΠΈΠΌΠΈ ΡΠΏΡΠ°Π²Π»ΡΠ΅Ρ.
|
|
182
224
|
|
|
183
225
|
---
|
|
184
226
|
|
|
185
|
-
##
|
|
227
|
+
## π₯ Π§ΡΠΎ ΠΌΠΎΠΆΠ½ΠΎ Π΄Π΅Π»Π°ΡΡ
|
|
228
|
+
|
|
229
|
+
ΠΡΠΎΡΡΠΎ ΠΏΠΎΠΏΡΠΎΡΠΈΡΠ΅ Gemini ΠΎΠ±ΡΡΠ½ΡΠΌ ΡΠ·ΡΠΊΠΎΠΌ, ΠΎΡΠΊΡΠ΄Π° ΡΠ³ΠΎΠ΄Π½ΠΎ:
|
|
186
230
|
|
|
187
|
-
|
|
231
|
+
- *Β«ΠΠΎΡΠΌΠΎΡΡΠΈ, ΠΏΠΎΡΠ΅ΠΌΡ ΡΠΎΡΠΌΠΎΠ·ΠΈΡ Π΄ΠΎΠΌΠ°ΡΠ½ΠΈΠΉ ΡΠ΅ΡΠ²Π΅Ρ, ΠΈ ΠΏΠΎΠΊΠ°ΠΆΠΈ ΡΠΎΠΏ ΠΏΡΠΎΡΠ΅ΡΡΠΎΠ²Β»*
|
|
232
|
+
- *Β«ΠΠΎΠ΄ΡΡΠ½ΠΈ ΠΏΠΎΡΠ»Π΅Π΄Π½ΠΈΠ΅ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ Π² ~/projects/api, ΠΏΡΠΎΠ³ΠΎΠ½ΠΈ ΡΠ΅ΡΡΡ ΠΈ ΡΠΊΠ°ΠΆΠΈ, ΡΡΠΎ ΡΠΏΠ°Π»ΠΎΒ»*
|
|
233
|
+
- *Β«ΠΡΠΏΡΠ°Π²Ρ ΠΎΠΏΠ΅ΡΠ°ΡΠΊΡ Π² config.yaml ΠΈ ΠΏΠ΅ΡΠ΅Π·Π°ΠΏΡΡΡΠΈ ΡΠ΅ΡΠ²ΠΈΡΒ»*
|
|
234
|
+
- *Β«ΠΠ°ΠΏΡΡΡΠΈ ΠΏΠΎΠ»Π½ΡΠΉ Π±ΡΠΊΠ°ΠΏ Π² ΡΠΎΠ½Π΅ ΠΈ ΡΠΎΠΎΠ±ΡΠΈ, ΠΊΠΎΠ³Π΄Π° Π·Π°ΠΊΠΎΠ½ΡΠΈΡΡΡΒ»*
|
|
235
|
+
- *Β«Π‘ΠΊΠΎΠ»ΡΠΊΠΎ ΡΠ²ΠΎΠ±ΠΎΠ΄Π½ΠΎΠ³ΠΎ ΠΌΠ΅ΡΡΠ° ΠΎΡΡΠ°Π»ΠΎΡΡ Π½Π° Π½ΠΎΡΡΠ±ΡΠΊΠ΅?Β»*
|
|
188
236
|
|
|
189
|
-
|
|
237
|
+
ΠΠ΅Π· SSH-ΠΊΠ»ΠΈΠ΅Π½ΡΠ°, Π±Π΅Π· ΡΠ΅ΡΠΌΠΈΠ½Π°Π»Π° Π½Π° ΡΠ΅Π»Π΅ΡΠΎΠ½Π΅, Π±Π΅Π· VPN β ΡΠΎΠ»ΡΠΊΠΎ ΡΠ°Ρ.
|
|
190
238
|
|
|
191
239
|
---
|
|
192
240
|
|
|
193
|
-
## β¨
|
|
241
|
+
## β¨ ΠΠΎΡΠ΅ΠΌΡ ΡΡΠΎ ΡΠ΄ΠΎΠ±Π½ΠΎ
|
|
194
242
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
- π» **ΠΡΠΎΡΡΠΏΠ»Π°ΡΡΠΎΡΠΌΠ΅Π½Π½ΠΎΡΡΡ**: ΠΠΎΠ»Π½Π°Ρ ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΊΠ° Linux, macOS ΠΈ Windows.
|
|
243
|
+
| | |
|
|
244
|
+
| :--- | :--- |
|
|
245
|
+
| π± **ΠΠΎΠ±ΠΈΠ»ΡΠ½ΡΠΉ Π°Π³Π΅Π½Ρ** | Π Π°Π±ΠΎΡΠ°Π΅Ρ Π² ΠΎΡΠΈΡΠΈΠ°Π»ΡΠ½ΠΎΠΌ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ Gemini ΠΈ Π² Π²Π΅Π±Π΅ ΡΠ΅ΡΠ΅Π· Gemini Spark. |
|
|
246
|
+
| π₯οΈ **ΠΡΠ±ΠΎΠΉ ΠΠ** | Linux, macOS, Windows β Π΄Π΅ΡΠΊΡΠΎΠΏΡ, Π½ΠΎΡΡΠ±ΡΠΊΠΈ, VPS, ΠΊΠΎΠ½ΡΠ΅ΠΉΠ½Π΅ΡΡ. |
|
|
247
|
+
| π **ΠΠ° Π»ΡΠ±ΡΠΌ NAT** | Π£Π·Π΅Π» ΡΠ°ΠΌ ΠΎΡΠΊΡΡΠ²Π°Π΅Ρ *ΠΈΡΡ
ΠΎΠ΄ΡΡΠΈΠΉ* WebSocket-ΡΡΠ½Π½Π΅Π»Ρ. ΠΠ΅ Π½ΡΠΆΠ½Ρ Π±Π΅Π»ΡΠΉ IP, ΠΏΡΠΎΠ±ΡΠΎΡ ΠΏΠΎΡΡΠΎΠ² ΠΈ Π½Π°ΡΡΡΠΎΠΉΠΊΠ° ΡΠΎΡΡΠ΅ΡΠ°. |
|
|
248
|
+
| β‘ **Π£ΡΡΠ°Π½ΠΎΠ²ΠΊΠ° ΠΎΠ΄Π½ΠΎΠΉ ΠΊΠΎΠΌΠ°Π½Π΄ΠΎΠΉ** | ΠΠΏΡΠ΅Π΄Π΅Π»ΡΠ΅Ρ ΠΠ‘, Π°ΡΡ
ΠΈΡΠ΅ΠΊΡΡΡΡ ΠΈ ΡΠΈΠΏ ΡΡΡΡΠΎΠΉΡΡΠ²Π°; ΡΡΠ°Π²ΠΈΡ Π°Π²ΡΠΎΠ·Π°ΠΏΡΡΠΊ (`systemd` / `launchd` / Π·Π°Π΄Π°ΡΠ° Windows); ΠΊΠΎΠΏΠΈΡΡΠ΅Ρ ΡΡΡΠ»ΠΊΡ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΡ Π² Π±ΡΡΠ΅Ρ ΠΎΠ±ΠΌΠ΅Π½Π°. |
|
|
249
|
+
| π‘οΈ **ΠΠΎΡΡΡΠΏ ΠΏΠΎ ΡΠΎΠΊΠ΅Π½Ρ** | ΠΠ°ΠΆΠ΄ΡΠΉ Π²ΡΠ·ΠΎΠ² ΡΡΠ΅Π±ΡΠ΅Ρ Π»ΠΈΡΠ½ΡΠΉ 128-Π±ΠΈΡΠ½ΡΠΉ ΡΠΎΠΊΠ΅Π½ (`?token=β¦` ΠΈΠ»ΠΈ `Authorization: Bearer`). ΠΡΡΠ°Π»ΡΠ½ΡΠΌ β HTTP 401. |
|
|
250
|
+
| π **ΠΠ½ΠΎΠ³ΠΎ ΠΌΠ°ΡΠΈΠ½, ΠΎΠ΄ΠΈΠ½ ΡΠ»ΡΠ·** | ΠΠ°ΠΆΠ΄ΡΠΉ ΡΠ·Π΅Π» Π²ΡΠ±ΠΈΡΠ°Π΅ΡΡΡ ΠΏΠ°ΡΠ°ΠΌΠ΅ΡΡΠΎΠΌ `?user=<ΠΈΠΌΡ-ΡΠ·Π»Π°>` Π½Π° ΠΎΠ΄Π½ΠΎΠΌ ΠΎΠ±ΡΠ΅ΠΌ Π΄ΠΎΠΌΠ΅Π½Π΅. |
|
|
251
|
+
| π **ΠΠ΅ΡΠΏΠ»Π°ΡΠ½ΠΎ ΠΈ ΠΎΡΠΊΡΡΡΠΎ** | ΠΠΈΡΠ΅Π½Π·ΠΈΡ MIT, ΡΠ°Π±ΠΎΡΠ°Π΅Ρ Π½Π° Π±Π΅ΡΠΏΠ»Π°ΡΠ½ΠΎΠΌ ΡΠ°ΡΠΈΡΠ΅ Gemini. |
|
|
205
252
|
|
|
206
253
|
---
|
|
207
254
|
|
|
208
|
-
## π οΈ
|
|
255
|
+
## π οΈ Π§ΡΠΎ Gemini ΠΌΠΎΠΆΠ΅Ρ Π΄Π΅Π»Π°ΡΡ Π½Π° Π²Π°ΡΠ΅ΠΉ ΠΌΠ°ΡΠΈΠ½Π΅ (MCP-ΠΈΠ½ΡΡΡΡΠΌΠ΅Π½ΡΡ)
|
|
209
256
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
257
|
+
| ΠΠ°ΡΠ΅Π³ΠΎΡΠΈΡ | ΠΠ½ΡΡΡΡΠΌΠ΅Π½ΡΡ |
|
|
258
|
+
| :--- | :--- |
|
|
259
|
+
| Π’Π΅ΡΠΌΠΈΠ½Π°Π» | `bash_exec(command)` |
|
|
260
|
+
| Π€Π°ΠΉΠ»Ρ | `list_dir(path)` Β· `read_file(path, start_line, end_line)` Β· `write_file(path, content)` Β· `edit_file(path, old_string, new_string)` |
|
|
261
|
+
| ΠΠΎΠΈΡΠΊ | `grep_search(pattern, path)` Β· `glob_find(pattern, path)` |
|
|
262
|
+
| ΠΠΎΠ»Π³ΠΈΠ΅ Π·Π°Π΄Π°ΡΠΈ | `run_job(command)` β `job_output(job_id)` Β· `job_kill(job_id)` Β· `job_list()` |
|
|
263
|
+
| Π‘ΠΎΡΡΠΎΡΠ½ΠΈΠ΅ | `system_vitals()` Β· `system_info()` Β· `mesh_status()` |
|
|
264
|
+
| ΠΠΎΠ²Π΅Π΄Π΅Π½ΠΈΠ΅ Π°Π³Π΅Π½ΡΠ° | `get_orchestration_skill()` β Π·Π°Π³ΡΡΠΆΠ°Π΅Ρ ΡΠ°Π±ΠΎΡΠΈΠ΅ ΠΏΡΠ°Π²ΠΈΠ»Π° Π°Π³Π΅Π½ΡΠ° |
|
|
265
|
+
|
|
266
|
+
> [!TIP]
|
|
267
|
+
> Gemini Π΄Π°ΡΡ ΠΎΠ΄Π½ΠΎΠΌΡ Π²ΡΠ·ΠΎΠ²Ρ ΠΈΠ½ΡΡΡΡΠΌΠ΅Π½ΡΠ° ΠΎΠΊΠΎΠ»ΠΎ 30 ΡΠ΅ΠΊΡΠ½Π΄. ΠΡΡ, ΡΡΠΎ Π΄ΠΎΠ»ΡΡΠ΅ (ΡΠ±ΠΎΡΠΊΠΈ, Π±ΡΠΊΠ°ΠΏΡ, Π·Π°Π³ΡΡΠ·ΠΊΠΈ), Π°Π³Π΅Π½Ρ Π·Π°ΠΏΡΡΠΊΠ°Π΅Ρ ΡΠ΅ΡΠ΅Π· `run_job` ΠΈ Π·Π°Π±ΠΈΡΠ°Π΅Ρ ΡΠ΅Π·ΡΠ»ΡΡΠ°Ρ ΡΠ΅ΡΠ΅Π· `job_output` β Π·Π°Π΄Π°ΡΠΈ Π½Π΅ ΠΎΠ±ΡΡΠ²Π°ΡΡΡΡ.
|
|
213
268
|
|
|
214
269
|
---
|
|
215
270
|
|
|
216
|
-
## π ΠΡΡΡΡΡΠΉ ΡΡΠ°ΡΡ
|
|
271
|
+
## π ΠΡΡΡΡΡΠΉ ΡΡΠ°ΡΡ β 2 ΠΌΠΈΠ½ΡΡΡ
|
|
272
|
+
|
|
273
|
+
### 1. Π£ΡΡΠ°Π½ΠΎΠ²ΠΈΡΠ΅ ΡΠ·Π΅Π» Π½Π° ΠΠ, ΠΊΠΎΡΠΎΡΡΠΌ Ρ
ΠΎΡΠΈΡΠ΅ ΡΠΏΡΠ°Π²Π»ΡΡΡ
|
|
217
274
|
|
|
218
|
-
|
|
275
|
+
**π§ Linux / π macOS**
|
|
219
276
|
```bash
|
|
220
277
|
curl -fsSL https://smart-server.online/install.sh | bash
|
|
221
278
|
```
|
|
222
279
|
|
|
223
|
-
|
|
280
|
+
**πͺ Windows (PowerShell)**
|
|
224
281
|
```powershell
|
|
225
282
|
irm https://smart-server.online/install.ps1 | iex
|
|
226
283
|
```
|
|
227
284
|
|
|
228
|
-
|
|
285
|
+
**π¦ Node.js (Π»ΡΠ±Π°Ρ ΠΠ‘)**
|
|
229
286
|
```bash
|
|
230
287
|
npx gemini-computer-use
|
|
231
288
|
```
|
|
232
289
|
|
|
233
|
-
|
|
290
|
+
**π οΈ ΠΠ· ΠΈΡΡ
ΠΎΠ΄Π½ΠΈΠΊΠΎΠ²**
|
|
234
291
|
```bash
|
|
235
292
|
git clone https://github.com/LevRa7/Computer-use-for-Gemini-App-Web.git
|
|
236
293
|
cd Computer-use-for-Gemini-App-Web
|
|
237
294
|
./install.sh --quick
|
|
238
295
|
```
|
|
239
296
|
|
|
297
|
+
Π ΠΊΠΎΠ½ΡΠ΅ ΡΡΡΠ°Π½ΠΎΠ²ΡΠΈΠΊ Π²ΡΠ²Π΅Π΄Π΅Ρ Π²Π°ΡΡ Π»ΠΈΡΠ½ΡΡ MCP-ΡΡΡΠ»ΠΊΡ ΠΈ ΡΠΊΠΎΠΏΠΈΡΡΠ΅Ρ Π΅Ρ Π² Π±ΡΡΠ΅Ρ ΠΎΠ±ΠΌΠ΅Π½Π°:
|
|
298
|
+
```text
|
|
299
|
+
https://smart-server.online/sse?user=<ΠΈΠΌΡ-Π²Π°ΡΠ΅Π³ΠΎ-ΡΠ·Π»Π°>&token=<Π²Π°Ρ_ΡΠ΅ΠΊΡΠ΅ΡΠ½ΡΠΉ_ΡΠΎΠΊΠ΅Π½>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### 2. ΠΠΎΠ΄ΠΊΠ»ΡΡΠΈΡΠ΅ Π΅Ρ ΠΊ Gemini Spark
|
|
303
|
+
|
|
304
|
+
1. ΠΡΠΊΡΠΎΠΉΡΠ΅ **[gemini.google.com/spark/apps](https://gemini.google.com/spark/apps)** Π² Π±ΡΠ°ΡΠ·Π΅ΡΠ΅ ΠΈΠ»ΠΈ Π² ΠΌΠΎΠ±ΠΈΠ»ΡΠ½ΠΎΠΌ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ Gemini.
|
|
305
|
+
2. ΠΠ°ΠΆΠΌΠΈΡΠ΅ **ΠΠΎΠ±Π°Π²ΠΈΡΡ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅** (ΠΈΠ»ΠΈ **ΠΠ°ΡΡΡΠΎΠΉΠΊΠΈ βοΈ β ΠΠ½ΡΡΡΡΠΌΠ΅Π½ΡΡ / Π Π°ΡΡΠΈΡΠ΅Π½ΠΈΡ (MCP)**).
|
|
306
|
+
3. ΠΡΡΠ°Π²ΡΡΠ΅ ΡΡΡΠ»ΠΊΡ (**Ctrl+V**) ΠΈ ΡΠΎΡ
ΡΠ°Π½ΠΈΡΠ΅.
|
|
307
|
+
|
|
308
|
+
### 3. ΠΠ°ΠΉΡΠ΅ Π·Π°Π΄Π°ΡΡ
|
|
309
|
+
|
|
310
|
+
> *Β«ΠΠΎΠΊΠ°ΠΆΠΈ system_vitals ΠΈ 5 ΡΠ°ΠΌΡΡ
Π±ΠΎΠ»ΡΡΠΈΡ
ΠΏΠ°ΠΏΠΎΠΊ Π² Π΄ΠΎΠΌΠ°ΡΠ½Π΅ΠΌ ΠΊΠ°ΡΠ°Π»ΠΎΠ³Π΅.Β»*
|
|
311
|
+
|
|
312
|
+
ΠΠΎΡΠΎΠ²ΠΎ β Gemini ΡΠ΅ΠΏΠ΅ΡΡ Π°Π³Π΅Π½Ρ, ΡΠ°Π±ΠΎΡΠ°ΡΡΠΈΠΉ Π½Π° Π²Π°ΡΠ΅ΠΉ ΠΌΠ°ΡΠΈΠ½Π΅. ΠΠΎΠ²ΡΠΎΡΠΈΡΠ΅ ΡΠ°Π³ 1 Π½Π° Π΄ΡΡΠ³ΠΈΡ
ΠΊΠΎΠΌΠΏΡΡΡΠ΅ΡΠ°Ρ
, ΡΡΠΎΠ±Ρ ΡΠΏΡΠ°Π²Π»ΡΡΡ ΠΈΠΌΠΈ Π²ΡΠ΅ΠΌΠΈ Ρ ΠΎΠ΄Π½ΠΎΠ³ΠΎ ΡΠ΅Π»Π΅ΡΠΎΠ½Π°.
|
|
313
|
+
|
|
240
314
|
---
|
|
241
315
|
|
|
242
|
-
##
|
|
316
|
+
## π₯οΈ Π Π΅ΠΆΠΈΠΌΡ ΡΠ°Π·Π²ΡΡΡΡΠ²Π°Π½ΠΈΡ
|
|
243
317
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
```
|
|
250
|
-
ΠΡΠ΅ ΡΠ·Π»Ρ ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΡΡ **ΠΎΠ΄ΠΈΠ½ ΠΎΠ±ΡΠΈΠΉ Π΄ΠΎΠΌΠ΅Π½**, Π° Π½ΡΠΆΠ½ΡΠΉ ΡΠ·Π΅Π» Π²ΡΠ±ΠΈΡΠ°Π΅ΡΡΡ ΠΏΠ°ΡΠ°ΠΌΠ΅ΡΡΠΎΠΌ `?user=` β ΡΡΠ±Π΄ΠΎΠΌΠ΅Π½Ρ Π΄Π»Ρ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΡΡΡΡΠΎΠΉΡΡΠ²Π° Π½Π΅ ΡΠΎΠ·Π΄Π°ΡΡΡΡ, ΠΈ Π΄ΠΎΠ±Π°Π²Π»ΡΡΡ ΡΡΠΎ-Π»ΠΈΠ±ΠΎ Π² TLS-ΡΠ΅ΡΡΠΈΡΠΈΠΊΠ°Ρ Π½Π΅ Π½ΡΠΆΠ½ΠΎ.
|
|
251
|
-
4. ΠΠΎΡΠΎΠ²ΠΎ! Π’Π΅ΠΏΠ΅ΡΡ Gemini ΠΌΠΎΠΆΠ΅Ρ Π²ΡΠΏΠΎΠ»Π½ΡΡΡ ΠΊΠΎΠΌΠ°Π½Π΄Ρ ΠΏΡΡΠΌΠΎ Π½Π° Π²Π°ΡΠ΅ΠΉ ΠΌΠ°ΡΠΈΠ½Π΅.
|
|
318
|
+
| Π Π΅ΠΆΠΈΠΌ | ΠΠΎΠΌΠ°Π½Π΄Π° | ΠΠΎΠ³Π΄Π° ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΡ |
|
|
319
|
+
| :--- | :--- | :--- |
|
|
320
|
+
| **Π¨Π»ΡΠ· + ΡΡΠ½Π½Π΅Π»Ρ** (ΡΠ΅ΠΊΠΎΠΌΠ΅Π½Π΄ΡΠ΅ΡΡΡ) | `./install.sh --quick` | ΠΠΎΠΌΠ°ΡΠ½ΠΈΠ΅ ΠΠ ΠΈ Π½ΠΎΡΡΠ±ΡΠΊΠΈ Π·Π° NAT β ΡΡΠ΅Π½Π°ΡΠΈΠΉ ΠΌΠΎΠ±ΠΈΠ»ΡΠ½ΠΎΠ³ΠΎ Π°Π³Π΅Π½ΡΠ°. |
|
|
321
|
+
| **Π£Π΄Π°Π»ΡΠ½Π½ΠΎ ΡΠ΅ΡΠ΅Π· SSH** | `./install.sh --ssh=user@host` | Π Π°Π·Π²Π΅ΡΠ½ΡΡΡ ΡΠ·Π΅Π» Π½Π° ΡΠ΄Π°Π»ΡΠ½Π½ΠΎΠΌ Linux-ΡΠ΅ΡΠ²Π΅ΡΠ΅ ΠΏΡΡΠΌΠΎ ΠΈΠ· ΡΠ΅ΡΠΌΠΈΠ½Π°Π»Π°. |
|
|
322
|
+
| **ΠΠΎΠΊΠ°Π»ΡΠ½ΡΠΉ Π°Π²ΡΠΎΠ½ΠΎΠΌΠ½ΡΠΉ** | `./install.sh --mode=standalone --port=8096` | Π§ΠΈΡΡΠΎ Π»ΠΎΠΊΠ°Π»ΡΠ½ΡΠΉ FastMCP-ΡΠ΅ΡΠ²Π΅Ρ Π½Π° `http://localhost:8096/sse`, Π±Π΅Π· ΠΎΠ±Π»Π°ΡΠ½ΠΎΠ³ΠΎ ΡΠ΅Π»Π΅Ρ. |
|
|
252
323
|
|
|
253
|
-
|
|
324
|
+
---
|
|
254
325
|
|
|
255
|
-
|
|
256
|
-
|
|
326
|
+
## π ΠΠ΄ΠΈΠ½ ΠΎΠ±ΡΠΈΠΉ Π΄ΠΎΠΌΠ΅Π½ (ΠΊΠ°Π½ΠΎΠ½ΠΈΡΠ΅ΡΠΊΠΈΠΉ ΡΠΎΡΠΌΠ°Ρ URL)
|
|
327
|
+
|
|
328
|
+
Π£Π·Π΅Π» **Π½ΠΈΠΊΠΎΠ³Π΄Π°** Π½Π΅ Π°Π΄ΡΠ΅ΡΡΠ΅ΡΡΡ ΡΠΎΠ±ΡΡΠ²Π΅Π½Π½ΡΠΌ ΠΈΠΌΠ΅Π½Π΅ΠΌ Ρ
ΠΎΡΡΠ°. Π¨Π»ΡΠ· ΠΏΡΠ±Π»ΠΈΠΊΡΠ΅Ρ ΠΎΠ΄ΠΈΠ½ ΠΎΠ±ΡΠΈΠΉ Π΄ΠΎΠΌΠ΅Π½, Π° ΠΈΠΌΡ ΡΠ·Π»Π° ΠΏΠ΅ΡΠ΅Π΄Π°ΡΡΡΡ ΠΏΠ°ΡΠ°ΠΌΠ΅ΡΡΠΎΠΌ:
|
|
257
329
|
|
|
258
330
|
| ΠΠ°Π·Π½Π°ΡΠ΅Π½ΠΈΠ΅ | ΠΠ°Π½ΠΎΠ½ΠΈΡΠ΅ΡΠΊΠΈΠΉ URL |
|
|
259
331
|
| :--- | :--- |
|
|
@@ -261,47 +333,45 @@ cd Computer-use-for-Gemini-App-Web
|
|
|
261
333
|
| MCP Streamable HTTP | `https://<ΠΎΠ±ΡΠΈΠΉ-Π΄ΠΎΠΌΠ΅Π½>/mcp?user=<ΠΈΠΌΡ-ΡΠ·Π»Π°>&token=<ΡΠΎΠΊΠ΅Π½>` |
|
|
262
334
|
| ΠΠ±ΡΠ°ΡΠ½ΡΠΉ ΡΡΠ½Π½Π΅Π»Ρ (Π°Π³Π΅Π½Ρ) | `wss://<ΠΎΠ±ΡΠΈΠΉ-Π΄ΠΎΠΌΠ΅Π½>/ws/tunnel?user=<ΠΈΠΌΡ-ΡΠ·Π»Π°>&token=<ΡΠΎΠΊΠ΅Π½>` |
|
|
263
335
|
|
|
264
|
-
ΠΠΎΡΠ΅ΠΌΡ ΡΠ°ΠΊ: ΠΊΠ°ΠΆΠ΄ΠΎΠΌΡ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡΠ΅Π»ΡΠ½ΠΎΠΌΡ ΠΈΠΌΠ΅Π½ΠΈ Ρ
ΠΎΡΡΠ° Π½ΡΠΆΠ½Π° ΡΠ²ΠΎΡ DNS-Π·Π°ΠΏΠΈΡΡ **ΠΈ** ΡΠ²ΠΎΠΉ
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
336
|
+
ΠΠΎΡΠ΅ΠΌΡ ΡΠ°ΠΊ: ΠΊΠ°ΠΆΠ΄ΠΎΠΌΡ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡΠ΅Π»ΡΠ½ΠΎΠΌΡ ΠΈΠΌΠ΅Π½ΠΈ Ρ
ΠΎΡΡΠ° Π½ΡΠΆΠ½Π° ΡΠ²ΠΎΡ DNS-Π·Π°ΠΏΠΈΡΡ **ΠΈ** ΡΠ²ΠΎΠΉ SAN Π² TLS-ΡΠ΅ΡΡΠΈΡΠΈΠΊΠ°ΡΠ΅. ΠΡΠ»ΠΈ ΠΈΠΌΠ΅Π½ΠΈ Π½Π΅Ρ Π² ΡΠ΅ΡΡΠΈΡΠΈΠΊΠ°ΡΠ΅, TLS-ΡΡΠΊΠΎΠΏΠΎΠΆΠ°ΡΠΈΠ΅ ΠΏΠ°Π΄Π°Π΅Ρ, ΠΈ ΠΊΠ»ΠΈΠ΅Π½Ρ Gemini ΠΏΠΎΠΊΠ°Π·ΡΠ²Π°Π΅Ρ Π½Π΅Π²Π½ΡΡΠ½ΡΡ ΠΎΡΠΈΠ±ΠΊΡ Β«Π½Π΅ ΡΠ΄Π°ΡΡΡΡ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠΈΡΡΡΡ ΠΊ Ρ
ΠΎΡΡΡΒ». Π‘ ΠΎΠ΄Π½ΠΈΠΌ ΠΎΠ±ΡΠΈΠΌ Π΄ΠΎΠΌΠ΅Π½ΠΎΠΌ ΡΠ΅ΡΡΠΈΡΠΈΠΊΠ°Ρ ΠΏΠΎΠΊΡΡΠ²Π°Π΅Ρ Π²ΡΠ΅ ΡΠ·Π»Ρ ΡΡΠ°Π·Ρ, Π° Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ ΡΠ·Π»Π° β ΡΡΠΎ ΡΠΎΠ»ΡΠΊΠΎ Π²ΡΠ·ΠΎΠ² ΡΠ΅Π³ΠΈΡΡΡΠ°ΡΠΈΠΈ.
|
|
337
|
+
|
|
338
|
+
- ΠΠ±ΡΠΈΠΉ Π΄ΠΎΠΌΠ΅Π½ ΠΏΠΎ ΡΠΌΠΎΠ»ΡΠ°Π½ΠΈΡ `smart-server.online`; ΠΏΠ΅ΡΠ΅ΠΎΠΏΡΠ΅Π΄Π΅Π»ΡΠ΅ΡΡΡ ΡΠ΅ΡΠ΅Π· `./install.sh --domain=<ΠΎΠ±ΡΠΈΠΉ-Π΄ΠΎΠΌΠ΅Π½>` (Π½Π° ΡΡΠΎΡΠΎΠ½Π΅ ΡΠ·Π»Π°) ΠΈΠ»ΠΈ `MESH_PUBLIC_URL` (Π½Π° ΡΡΠΎΡΠΎΠ½Π΅ ΡΠ»ΡΠ·Π°).
|
|
339
|
+
- Π‘ΡΠ°ΡΡΠ΅ ΡΡΡΠ»ΠΊΠΈ Ρ ΡΡΠ±Π΄ΠΎΠΌΠ΅Π½Π°ΠΌΠΈ ΡΡΡΡΠΎΠΉΡΡΠ²Π° ΠΏΡΠΎΠ΄ΠΎΠ»ΠΆΠ°ΡΡ ΡΠ°Π±ΠΎΡΠ°ΡΡ Π΄Π»Ρ ΡΠΎΠ²ΠΌΠ΅ΡΡΠΈΠΌΠΎΡΡΠΈ ΠΈ ΠΏΠΈΡΡΡ ΠΏΡΠ΅Π΄ΡΠΏΡΠ΅ΠΆΠ΄Π΅Π½ΠΈΠ΅ Π² Π»ΠΎΠ³; `MESH_LEGACY_SUBDOMAIN=0` Π½Π° ΡΠ»ΡΠ·Π΅ ΠΎΡΠΊΠ»ΡΡΠ°Π΅Ρ ΠΈΡ
.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## π ΠΠ΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡΡ
|
|
344
|
+
|
|
345
|
+
- **ΠΠΎΡΡΡΠΏ ΡΠΎΠ»ΡΠΊΠΎ ΠΏΠΎ ΡΠΎΠΊΠ΅Π½Ρ** β ΠΊΠ°ΠΆΠ΄ΡΠΉ Π·Π°ΠΏΡΠΎΡ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅ΡΠΆΠ°ΡΡ ΡΠ΅ΠΊΡΠ΅ΡΠ½ΡΠΉ ΡΠΎΠΊΠ΅Π½ ΡΠ·Π»Π°.
|
|
346
|
+
- **Π’ΠΎΠ»ΡΠΊΠΎ ΠΈΡΡ
ΠΎΠ΄ΡΡΠΈΠΉ ΡΡΠ½Π½Π΅Π»Ρ** β ΡΠ·Π»Ρ Π½Π΅ ΡΠ»ΡΡΠ°ΡΡ ΠΏΡΠ±Π»ΠΈΡΠ½ΡΠ΅ ΠΏΠΎΡΡΡ; Π°Π³Π΅Π½Ρ ΡΠ°ΠΌ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ°Π΅ΡΡΡ ΠΊ ΡΠ»ΡΠ·Ρ.
|
|
347
|
+
- **Π¨Π»ΡΠ· β ΡΠΎΠ»ΡΠΊΠΎ ΡΠ΅Π»Π΅ΠΉ** β ΠΎΠ½ ΡΠ΅ΡΠΌΠΈΠ½ΠΈΡΡΠ΅Ρ TLS ΠΈ ΠΏΠ΅ΡΠ΅Π΄Π°ΡΡ Π²ΡΠ·ΠΎΠ²Ρ; ΠΊΠΎΠΌΠ°Π½Π΄Ρ Π²ΡΠΏΠΎΠ»Π½ΡΡΡΡΡ ΡΠΎΠ»ΡΠΊΠΎ Π½Π° Π²Π°ΡΠ΅ΠΌ ΡΠ·Π»Π΅.
|
|
348
|
+
- **ΠΠ΅Π· root ΠΏΠΎ ΡΠΌΠΎΠ»ΡΠ°Π½ΠΈΡ** β Π°Π³Π΅Π½Ρ ΡΠ°Π±ΠΎΡΠ°Π΅Ρ Π² ΠΏΡΠΎΡΡΡΠ°Π½ΡΡΠ²Π΅ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ.
|
|
269
349
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
(Π½Π° ΡΡΠΎΡΠΎΠ½Π΅ ΡΠ»ΡΠ·Π°).
|
|
273
|
-
- Π‘ΡΠ°ΡΡΠ΅ ΡΡΡΠ»ΠΊΠΈ Ρ ΡΡΠ±Π΄ΠΎΠΌΠ΅Π½Π°ΠΌΠΈ ΡΡΡΡΠΎΠΉΡΡΠ²Π° ΠΏΡΠΎΠ΄ΠΎΠ»ΠΆΠ°ΡΡ ΡΠ°Π±ΠΎΡΠ°ΡΡ Π΄Π»Ρ ΡΠΎΠ²ΠΌΠ΅ΡΡΠΈΠΌΠΎΡΡΠΈ
|
|
274
|
-
ΠΈ ΠΏΠΈΡΡΡ ΠΏΡΠ΅Π΄ΡΠΏΡΠ΅ΠΆΠ΄Π΅Π½ΠΈΠ΅ Π² Π»ΠΎΠ³; `MESH_LEGACY_SUBDOMAIN=0` Π½Π° ΡΠ»ΡΠ·Π΅ ΠΎΡΠΊΠ»ΡΡΠ°Π΅Ρ ΠΈΡ
.
|
|
350
|
+
> [!WARNING]
|
|
351
|
+
> ΠΡΠ±ΠΎΠΉ, Ρ ΠΊΠΎΠ³ΠΎ Π΅ΡΡΡ Π²Π°ΡΠ° MCP-ΡΡΡΠ»ΠΊΠ°, ΠΌΠΎΠΆΠ΅Ρ Π²ΡΠΏΠΎΠ»Π½ΡΡΡ ΠΊΠΎΠΌΠ°Π½Π΄Ρ Π½Π° ΡΡΠΎΠΉ ΠΌΠ°ΡΠΈΠ½Π΅. Π₯ΡΠ°Π½ΠΈΡΠ΅ Π΅Ρ ΠΊΠ°ΠΊ ΠΏΠ°ΡΠΎΠ»Ρ: Π½Π΅ ΠΏΡΠ±Π»ΠΈΠΊΡΠΉΡΠ΅, Π° ΠΏΡΠΈ ΡΡΠ΅ΡΠΊΠ΅ ΠΏΠ΅ΡΠ΅ΡΡΡΠ°Π½ΠΎΠ²ΠΈΡΠ΅ ΡΠ·Π΅Π», ΡΡΠΎΠ±Ρ ΡΠΌΠ΅Π½ΠΈΡΡ ΡΠΎΠΊΠ΅Π½.
|
|
275
352
|
|
|
276
353
|
---
|
|
277
354
|
|
|
278
|
-
## π’
|
|
355
|
+
## π’ Π‘Π²ΠΎΠΉ ΡΠ»ΡΠ· (self-hosting)
|
|
279
356
|
|
|
280
|
-
Π¨Π»ΡΠ· β ΠΎΠ΄ΠΈΠ½ Ρ
ΠΎΡΡ Π½Π° Π²ΡΠ΅ ΡΠ·Π»Ρ. ΠΡΠ±Π»ΠΈΠΊΡΠΉΡΠ΅ Π΅Π³ΠΎ ΡΠΎΠ»ΡΠΊΠΎ ΡΠΊΡΠΈΠΏΡΠΎΠΌ, ΡΡΠΎΠ±Ρ ΡΠ°Π±ΠΎΡΠ°ΡΡΠΈΠΉ
|
|
281
|
-
ΡΠ»ΡΠ·, ΠΎΡΠ΄Π°Π²Π°Π΅ΠΌΡΠ΅ ΡΡΡΠ°Π½ΠΎΠ²ΡΠΈΠΊΠΈ ΠΈ bootstrap-ΠΊΠΎΠ΄ ΡΠ·Π»ΠΎΠ² Π½Π΅ ΡΠ°ΡΡ
ΠΎΠ΄ΠΈΠ»ΠΈΡΡ:
|
|
357
|
+
Π¨Π»ΡΠ· β ΠΎΠ΄ΠΈΠ½ Ρ
ΠΎΡΡ Π½Π° Π²ΡΠ΅ ΡΠ·Π»Ρ. ΠΡΠ±Π»ΠΈΠΊΡΠΉΡΠ΅ Π΅Π³ΠΎ ΡΠΎΠ»ΡΠΊΠΎ ΡΠΊΡΠΈΠΏΡΠΎΠΌ, ΡΡΠΎΠ±Ρ ΡΠ°Π±ΠΎΡΠ°ΡΡΠΈΠΉ ΡΠ»ΡΠ·, ΠΎΡΠ΄Π°Π²Π°Π΅ΠΌΡΠ΅ ΡΡΡΠ°Π½ΠΎΠ²ΡΠΈΠΊΠΈ ΠΈ bootstrap-ΠΊΠΎΠ΄ ΡΠ·Π»ΠΎΠ² Π½Π΅ ΡΠ°ΡΡ
ΠΎΠ΄ΠΈΠ»ΠΈΡΡ:
|
|
282
358
|
|
|
283
359
|
```bash
|
|
284
360
|
MESH_GATEWAY_SSH=root@<ip-ΡΠ»ΡΠ·Π°> ./deploy_gateway.sh # Π·Π°Π΄Π΅ΠΏΠ»ΠΎΠΈΡΡ
|
|
285
361
|
MESH_GATEWAY_SSH=root@<ip-ΡΠ»ΡΠ·Π°> ./deploy_gateway.sh --dry-run # ΠΏΡΠ΅Π΄ΠΏΡΠΎΡΠΌΠΎΡΡ
|
|
286
362
|
```
|
|
287
|
-
Π‘ΠΊΡΠΈΠΏΡ ΠΏΡΠΎΠ²Π΅ΡΡΠ΅Ρ ΠΊΠ°ΠΆΠ΄ΡΡ Π·Π°Π³ΡΡΠ·ΠΊΡ ΠΌΠ°Π½ΠΈΡΠ΅ΡΡΠΎΠΌ sha256 Π½Π° Ρ
ΠΎΡΡΠ΅, Π΄Π΅Π»Π°Π΅Ρ Π±ΡΠΊΠ°ΠΏ Ρ
|
|
288
|
-
ΠΌΠ΅ΡΠΊΠΎΠΉ Π²ΡΠ΅ΠΌΠ΅Π½ΠΈ, ΡΡΠ°Π²ΠΈΡ ΡΠ°ΠΉΠ»Ρ Ρ Π½ΡΠΆΠ½ΡΠΌ Π²Π»Π°Π΄Π΅Π»ΡΡΠ΅ΠΌ, ΠΏΠ΅ΡΠ΅Π·Π°ΠΏΡΡΠΊΠ°Π΅Ρ ΡΠ»ΡΠΆΠ±Ρ ΠΈ
|
|
289
|
-
Π·Π°Π²Π΅ΡΡΠ°Π΅Ρ ΠΏΡΠ±Π»ΠΈΡΠ½ΠΎΠΉ ΠΏΡΠΎΠ²Π΅ΡΠΊΠΎΠΉ Π·Π΄ΠΎΡΠΎΠ²ΡΡ. ΠΡΠΈ Π½Π΅ΡΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠΈ Ρ
ΡΡΠ΅ΠΉ Π½ΠΈΡΠ΅Π³ΠΎ Π½Π΅
|
|
290
|
-
ΡΡΡΠ°Π½Π°Π²Π»ΠΈΠ²Π°Π΅ΡΡΡ.
|
|
363
|
+
Π‘ΠΊΡΠΈΠΏΡ ΠΏΡΠΎΠ²Π΅ΡΡΠ΅Ρ ΠΊΠ°ΠΆΠ΄ΡΡ Π·Π°Π³ΡΡΠ·ΠΊΡ ΠΌΠ°Π½ΠΈΡΠ΅ΡΡΠΎΠΌ sha256 Π½Π° Ρ
ΠΎΡΡΠ΅, Π΄Π΅Π»Π°Π΅Ρ Π±ΡΠΊΠ°ΠΏ Ρ ΠΌΠ΅ΡΠΊΠΎΠΉ Π²ΡΠ΅ΠΌΠ΅Π½ΠΈ, ΡΡΠ°Π²ΠΈΡ ΡΠ°ΠΉΠ»Ρ Ρ Π½ΡΠΆΠ½ΡΠΌ Π²Π»Π°Π΄Π΅Π»ΡΡΠ΅ΠΌ, ΠΏΠ΅ΡΠ΅Π·Π°ΠΏΡΡΠΊΠ°Π΅Ρ ΡΠ»ΡΠΆΠ±Ρ ΠΈ Π·Π°Π²Π΅ΡΡΠ°Π΅Ρ ΠΏΡΠ±Π»ΠΈΡΠ½ΠΎΠΉ ΠΏΡΠΎΠ²Π΅ΡΠΊΠΎΠΉ Π·Π΄ΠΎΡΠΎΠ²ΡΡ. ΠΡΠΈ Π½Π΅ΡΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠΈ Ρ
ΡΡΠ΅ΠΉ Π½ΠΈΡΠ΅Π³ΠΎ Π½Π΅ ΡΡΡΠ°Π½Π°Π²Π»ΠΈΠ²Π°Π΅ΡΡΡ.
|
|
291
364
|
|
|
292
|
-
**ΠΠΎΠ»ΠΈΡΠΈΠΊΠ° TLS-ΡΠ΅ΡΡΠΈΡΠΈΠΊΠ°ΡΠ°: ΡΠΎΠ»ΡΠΊΠΎ ΠΎΠ±ΡΠΈΠΉ Π΄ΠΎΠΌΠ΅Π½, Π±Π΅Π· SAN Π½Π° ΡΡΡΡΠΎΠΉΡΡΠ²Π°.**
|
|
293
|
-
ΠΠΌΠ΅Π½Π° ΡΠ·Π»ΠΎΠ² Π² ΡΠ΅ΡΡΠΈΡΠΈΠΊΠ°Ρ Π½Π΅ Π΄ΠΎΠ±Π°Π²Π»ΡΡΡΡΡ Π½ΠΈΠΊΠΎΠ³Π΄Π° β ΡΠ·Π΅Π» Π²ΡΠ±ΠΈΡΠ°Π΅ΡΡΡ `?user=`:
|
|
365
|
+
**ΠΠΎΠ»ΠΈΡΠΈΠΊΠ° TLS-ΡΠ΅ΡΡΠΈΡΠΈΠΊΠ°ΡΠ°: ΡΠΎΠ»ΡΠΊΠΎ ΠΎΠ±ΡΠΈΠΉ Π΄ΠΎΠΌΠ΅Π½, Π±Π΅Π· SAN Π½Π° ΡΡΡΡΠΎΠΉΡΡΠ²Π°.** ΠΠΌΠ΅Π½Π° ΡΠ·Π»ΠΎΠ² Π² ΡΠ΅ΡΡΠΈΡΠΈΠΊΠ°Ρ Π½Π΅ Π΄ΠΎΠ±Π°Π²Π»ΡΡΡΡΡ Π½ΠΈΠΊΠΎΠ³Π΄Π° β ΡΠ·Π΅Π» Π²ΡΠ±ΠΈΡΠ°Π΅ΡΡΡ `?user=`:
|
|
294
366
|
|
|
295
367
|
```bash
|
|
296
368
|
sudo certbot certificates | grep -A1 'Certificate Name: smart-server.online'
|
|
297
369
|
sudo certbot renew --dry-run --cert-name smart-server.online # ΠΏΡΠΎΠ²Π΅ΡΠΊΠ° ΠΏΡΠΎΠ΄Π»Π΅Π½ΠΈΡ
|
|
298
370
|
```
|
|
299
|
-
Π‘ΡΠ°ΡΡΠ΅ SAN'Ρ ΡΡΡΡΠΎΠΉΡΡΠ² ΠΌΠΎΠ³ΡΡ ΠΎΡΡΠ°ΡΡΡΡ Ρ ΠΏΡΠ΅ΠΆΠ½Π΅Π³ΠΎ ΠΊΠΎΠ½ΡΡΠ°ΠΊΡΠ°: ΠΎΠ½ΠΈ Π±Π΅Π·Π²ΡΠ΅Π΄Π½Ρ ΠΈ
|
|
300
|
-
ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΈΠ²Π°ΡΡ ΡΠΎΠ²ΠΌΠ΅ΡΡΠΈΠΌΠΎΡΡΡ ΡΡΠ°ΡΡΡ
ΡΡΡΠ»ΠΎΠΊ. Π£Π±ΡΠ°ΡΡ ΠΈΡ
ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡΠΈ ΡΠ»Π΅Π΄ΡΡΡΠ΅ΠΌ
|
|
301
|
-
ΠΏΡΠΎΠ΄Π»Π΅Π½ΠΈΠΈ, ΠΊΠΎΠ³Π΄Π° ΡΡΠ±Π΄ΠΎΠΌΠ΅Π½Π½ΡΠΌΠΈ ΡΡΡΠ»ΠΊΠ°ΠΌΠΈ Π½ΠΈΠΊΡΠΎ Π½Π΅ ΠΏΠΎΠ»ΡΠ·ΡΠ΅ΡΡΡ.
|
|
371
|
+
Π‘ΡΠ°ΡΡΠ΅ SAN'Ρ ΡΡΡΡΠΎΠΉΡΡΠ² ΠΌΠΎΠ³ΡΡ ΠΎΡΡΠ°ΡΡΡΡ Ρ ΠΏΡΠ΅ΠΆΠ½Π΅Π³ΠΎ ΠΊΠΎΠ½ΡΡΠ°ΠΊΡΠ°: ΠΎΠ½ΠΈ Π±Π΅Π·Π²ΡΠ΅Π΄Π½Ρ ΠΈ ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΈΠ²Π°ΡΡ ΡΠΎΠ²ΠΌΠ΅ΡΡΠΈΠΌΠΎΡΡΡ ΡΡΠ°ΡΡΡ
ΡΡΡΠ»ΠΎΠΊ. Π£Π±ΡΠ°ΡΡ ΠΈΡ
ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡΠΈ ΡΠ»Π΅Π΄ΡΡΡΠ΅ΠΌ ΠΏΡΠΎΠ΄Π»Π΅Π½ΠΈΠΈ, ΠΊΠΎΠ³Π΄Π° ΡΡΠ±Π΄ΠΎΠΌΠ΅Π½Π½ΡΠΌΠΈ ΡΡΡΠ»ΠΊΠ°ΠΌΠΈ Π½ΠΈΠΊΡΠΎ Π½Π΅ ΠΏΠΎΠ»ΡΠ·ΡΠ΅ΡΡΡ.
|
|
302
372
|
|
|
303
373
|
---
|
|
304
374
|
|
|
305
375
|
## π ΠΠΈΡΠ΅Π½Π·ΠΈΡ
|
|
306
376
|
|
|
307
|
-
|
|
377
|
+
**MIT** β ΠΏΠΎΠ΄ΡΠΎΠ±Π½ΠΎΡΡΠΈ Π² ΡΠ°ΠΉΠ»Π΅ [LICENSE](LICENSE).
|
package/core/agent.py
CHANGED
|
@@ -7,9 +7,27 @@ so an unmodified gateway keeps working.
|
|
|
7
7
|
|
|
8
8
|
Configuration (read once at import/startup)::
|
|
9
9
|
|
|
10
|
+
MESH_GATEWAY shared gateway domain
|
|
11
|
+
MESH_USER node name
|
|
12
|
+
MESH_TOKEN node token
|
|
13
|
+
MESH_CONFIG_FILE KEY=VALUE file used for any of the three above that is not
|
|
14
|
+
set in the environment (default:
|
|
15
|
+
~/.config/antigravity-mesh/agent.env). This keeps the token
|
|
16
|
+
out of launchers such as the Windows Startup script.
|
|
10
17
|
MESH_WORKSPACE base directory for relative paths (default: cwd)
|
|
11
18
|
MESH_READ_ONLY "1"/"true" -> mutating tools refuse to run
|
|
12
19
|
MESH_WRITE_ROOTS pathsep-separated list of roots allowed for writes
|
|
20
|
+
|
|
21
|
+
Resilience contract:
|
|
22
|
+
|
|
23
|
+
* every link failure (DNS, TLS, reset, half-open socket after sleep) is retried
|
|
24
|
+
forever with capped exponential backoff plus jitter;
|
|
25
|
+
* tool calls run concurrently, so a slow command never delays another call or
|
|
26
|
+
the keepalive pings;
|
|
27
|
+
* only one agent per node name runs on a machine. The gateway keeps exactly one
|
|
28
|
+
tunnel per node and a new tunnel supersedes the old one, so two agents would
|
|
29
|
+
knock each other off in an endless loop and fail every in-flight call. A
|
|
30
|
+
second instance therefore waits for the lock instead of connecting.
|
|
13
31
|
"""
|
|
14
32
|
|
|
15
33
|
import asyncio
|
|
@@ -17,6 +35,8 @@ import json
|
|
|
17
35
|
import logging
|
|
18
36
|
import os
|
|
19
37
|
import random
|
|
38
|
+
import sys
|
|
39
|
+
import time
|
|
20
40
|
|
|
21
41
|
import websockets
|
|
22
42
|
|
|
@@ -25,9 +45,49 @@ from core import mcp_tools
|
|
|
25
45
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
26
46
|
logger = logging.getLogger("agy-agent")
|
|
27
47
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".config", "antigravity-mesh")
|
|
49
|
+
DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, "agent.env")
|
|
50
|
+
|
|
51
|
+
#: Backoff cap for ordinary link failures.
|
|
52
|
+
MAX_BACKOFF = 15.0
|
|
53
|
+
#: Wait after the gateway rejects the token: retrying fast cannot help, but the
|
|
54
|
+
#: node must recover by itself once it is re-registered.
|
|
55
|
+
UNAUTHORIZED_BACKOFF = 60.0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_env_file(path: str) -> dict:
|
|
59
|
+
"""Parse a KEY=VALUE file; tolerates a UTF-8 BOM (Windows PowerShell 5 writes one)."""
|
|
60
|
+
values = {}
|
|
61
|
+
try:
|
|
62
|
+
with open(path, "r", encoding="utf-8-sig") as handle:
|
|
63
|
+
for line in handle:
|
|
64
|
+
line = line.strip()
|
|
65
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
66
|
+
continue
|
|
67
|
+
key, _, value = line.partition("=")
|
|
68
|
+
values[key.strip()] = value.strip().strip('"').strip("'")
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
return values
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _load_settings() -> dict:
|
|
75
|
+
file_values = read_env_file(os.environ.get("MESH_CONFIG_FILE") or DEFAULT_CONFIG_FILE)
|
|
76
|
+
|
|
77
|
+
def pick(key: str, default: str) -> str:
|
|
78
|
+
return os.environ.get(key) or file_values.get(key) or default
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
"gateway": pick("MESH_GATEWAY", "smart-server.online"),
|
|
82
|
+
"user": pick("MESH_USER", "levra7"),
|
|
83
|
+
"token": pick("MESH_TOKEN", ""),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
_SETTINGS = _load_settings()
|
|
88
|
+
GATEWAY_HOST = _SETTINGS["gateway"]
|
|
89
|
+
USER = _SETTINGS["user"]
|
|
90
|
+
TOKEN = _SETTINGS["token"]
|
|
31
91
|
|
|
32
92
|
|
|
33
93
|
def _env_flag(name: str) -> bool:
|
|
@@ -53,12 +113,113 @@ def configure_from_env() -> None:
|
|
|
53
113
|
configure_from_env()
|
|
54
114
|
|
|
55
115
|
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# Single instance per node name
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
class InstanceLock:
|
|
121
|
+
"""Non-blocking, OS-released file lock (fcntl on POSIX, msvcrt on Windows).
|
|
122
|
+
|
|
123
|
+
The OS drops the lock when the process dies, so a crash never leaves a stale
|
|
124
|
+
lock behind.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, path: str):
|
|
128
|
+
self.path = path
|
|
129
|
+
self._fh = None
|
|
130
|
+
|
|
131
|
+
def try_acquire(self) -> bool:
|
|
132
|
+
os.makedirs(os.path.dirname(self.path), exist_ok=True)
|
|
133
|
+
fh = open(self.path, "a+")
|
|
134
|
+
try:
|
|
135
|
+
if os.name == "nt":
|
|
136
|
+
import msvcrt
|
|
137
|
+
fh.seek(0)
|
|
138
|
+
msvcrt.locking(fh.fileno(), msvcrt.LK_NBLCK, 1)
|
|
139
|
+
else:
|
|
140
|
+
import fcntl
|
|
141
|
+
fcntl.flock(fh.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
142
|
+
except OSError:
|
|
143
|
+
fh.close()
|
|
144
|
+
return False
|
|
145
|
+
fh.seek(0)
|
|
146
|
+
fh.truncate()
|
|
147
|
+
fh.write(str(os.getpid()))
|
|
148
|
+
fh.flush()
|
|
149
|
+
self._fh = fh
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def wait_for_instance_lock(user: str) -> InstanceLock:
|
|
154
|
+
"""Block until this process is the only agent for ``user`` on the machine."""
|
|
155
|
+
safe = "".join(c for c in user if c.isalnum() or c in "-_") or "node"
|
|
156
|
+
lock = InstanceLock(os.path.join(CONFIG_DIR, "agent-%s.lock" % safe))
|
|
157
|
+
warned = False
|
|
158
|
+
while not lock.try_acquire():
|
|
159
|
+
if not warned:
|
|
160
|
+
logger.warning("Another agent for node '%s' is already running on this machine; "
|
|
161
|
+
"waiting for it to exit instead of fighting over the tunnel.", user)
|
|
162
|
+
warned = True
|
|
163
|
+
time.sleep(10)
|
|
164
|
+
return lock
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Tunnel
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
|
|
56
171
|
async def handle_tool_call(name: str, args: dict) -> dict:
|
|
57
172
|
"""Forward one tool call to the shared implementation in a worker thread."""
|
|
58
173
|
logger.info("Executing %s", name)
|
|
59
174
|
return await asyncio.to_thread(mcp_tools.call_tool, name, args or {})
|
|
60
175
|
|
|
61
176
|
|
|
177
|
+
async def _serve_call(ws, req_id, params: dict) -> None:
|
|
178
|
+
tool_name = params.get("name")
|
|
179
|
+
tool_args = params.get("arguments", {})
|
|
180
|
+
try:
|
|
181
|
+
res = await handle_tool_call(tool_name, tool_args)
|
|
182
|
+
except Exception as tool_exc:
|
|
183
|
+
# A tool must never take the tunnel down with it.
|
|
184
|
+
logger.warning(f"Tool {tool_name} failed: {tool_exc}")
|
|
185
|
+
res = {"error": f"{type(tool_exc).__name__}: {tool_exc}"}
|
|
186
|
+
try:
|
|
187
|
+
await ws.send(json.dumps({"id": req_id, "result": res}, ensure_ascii=False))
|
|
188
|
+
except Exception as send_exc:
|
|
189
|
+
# The link dropped while the tool ran; the gateway already failed the
|
|
190
|
+
# call, and the reconnect loop restores the tunnel.
|
|
191
|
+
logger.warning(f"Could not deliver result of {tool_name}: {send_exc}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _is_unauthorized(exc: BaseException) -> bool:
|
|
195
|
+
"""True when the gateway rejected the node's credentials.
|
|
196
|
+
|
|
197
|
+
The gateway closes with 4001 before accepting the websocket; Starlette turns
|
|
198
|
+
that into an HTTP 403 handshake response, so depending on timing the client
|
|
199
|
+
sees either the close code or the HTTP status. Handles websockets' legacy
|
|
200
|
+
and new exception shapes.
|
|
201
|
+
"""
|
|
202
|
+
for attr in ("rcvd", "received"):
|
|
203
|
+
frame = getattr(exc, attr, None)
|
|
204
|
+
if frame is not None and getattr(frame, "code", None) == 4001:
|
|
205
|
+
return True
|
|
206
|
+
if getattr(exc, "code", None) == 4001:
|
|
207
|
+
return True
|
|
208
|
+
status = getattr(exc, "status_code", None) # legacy InvalidStatusCode
|
|
209
|
+
response = getattr(exc, "response", None) # websockets >= 14 InvalidStatus
|
|
210
|
+
if status is None and response is not None:
|
|
211
|
+
status = getattr(response, "status_code", None)
|
|
212
|
+
return status in (401, 403)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def tunnel_uri() -> str:
|
|
216
|
+
"""Canonical tunnel URL: one shared domain, node selected by ?user=."""
|
|
217
|
+
base = GATEWAY_HOST.rstrip("/")
|
|
218
|
+
if not base.startswith(("ws://", "wss://")):
|
|
219
|
+
base = "wss://" + base
|
|
220
|
+
return f"{base}/ws/tunnel?user={USER}&token={TOKEN}"
|
|
221
|
+
|
|
222
|
+
|
|
62
223
|
async def run_agent():
|
|
63
224
|
"""Stay connected to the gateway, healing every kind of link failure.
|
|
64
225
|
|
|
@@ -67,13 +228,18 @@ async def run_agent():
|
|
|
67
228
|
hammering loop when the network is genuinely down. The backoff resets after a
|
|
68
229
|
healthy connection, so a long-lived session is not penalised.
|
|
69
230
|
"""
|
|
70
|
-
|
|
71
|
-
|
|
231
|
+
if not TOKEN:
|
|
232
|
+
logger.error("MESH_TOKEN is empty (env and %s): the gateway will reject this node. "
|
|
233
|
+
"Re-run the installer.", os.environ.get("MESH_CONFIG_FILE") or DEFAULT_CONFIG_FILE)
|
|
234
|
+
uri = tunnel_uri()
|
|
235
|
+
logger.info(f"Connecting to Gateway {uri.split('&token=')[0]} ...")
|
|
72
236
|
backoff = 1.0
|
|
237
|
+
in_flight = set()
|
|
73
238
|
while True:
|
|
74
239
|
try:
|
|
75
240
|
async with websockets.connect(uri, ping_interval=10, ping_timeout=10,
|
|
76
|
-
close_timeout=5
|
|
241
|
+
close_timeout=5, open_timeout=15,
|
|
242
|
+
max_size=None) as ws:
|
|
77
243
|
logger.info(f"Connected to Mesh Gateway as '{USER}'!")
|
|
78
244
|
backoff = 1.0 # healthy again
|
|
79
245
|
async for raw_msg in ws:
|
|
@@ -81,26 +247,36 @@ async def run_agent():
|
|
|
81
247
|
data = json.loads(raw_msg)
|
|
82
248
|
except Exception:
|
|
83
249
|
continue
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
res = {"error": f"{type(tool_exc).__name__}: {tool_exc}"}
|
|
96
|
-
resp = {"id": req_id, "result": res}
|
|
97
|
-
await ws.send(json.dumps(resp, ensure_ascii=False))
|
|
250
|
+
if data.get("method") == "tools/call":
|
|
251
|
+
# Each call runs on its own: Gemini issues parallel calls,
|
|
252
|
+
# and serialising them pushed later ones past the
|
|
253
|
+
# gateway's 28 s limit.
|
|
254
|
+
task = asyncio.create_task(_serve_call(ws, data.get("id"), data.get("params", {})))
|
|
255
|
+
in_flight.add(task)
|
|
256
|
+
task.add_done_callback(in_flight.discard)
|
|
257
|
+
# A clean close (e.g. gateway restart) still needs a reconnect.
|
|
258
|
+
raise ConnectionError("tunnel closed by gateway")
|
|
259
|
+
except asyncio.CancelledError:
|
|
260
|
+
raise
|
|
98
261
|
except Exception as e:
|
|
99
|
-
|
|
100
|
-
|
|
262
|
+
if _is_unauthorized(e):
|
|
263
|
+
delay = UNAUTHORIZED_BACKOFF
|
|
264
|
+
logger.error("Gateway rejected node '%s' (4001 Unauthorized): the token is wrong or the "
|
|
265
|
+
"node was removed. Re-run the installer. Retrying in %.0fs.", USER, delay)
|
|
266
|
+
else:
|
|
267
|
+
delay = min(backoff, MAX_BACKOFF) + random.uniform(0, 0.5)
|
|
268
|
+
logger.warning(f"Connection lost: {e}. Reconnecting in {delay:.1f}s...")
|
|
269
|
+
backoff = min(backoff * 2, MAX_BACKOFF)
|
|
101
270
|
await asyncio.sleep(delay)
|
|
102
|
-
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def main() -> None:
|
|
274
|
+
_lock = wait_for_instance_lock(USER) # held for the life of the process
|
|
275
|
+
try:
|
|
276
|
+
asyncio.run(run_agent())
|
|
277
|
+
except KeyboardInterrupt:
|
|
278
|
+
sys.exit(0)
|
|
103
279
|
|
|
104
280
|
|
|
105
281
|
if __name__ == "__main__":
|
|
106
|
-
|
|
282
|
+
main()
|
package/core/mcp_tools.py
CHANGED
|
@@ -52,6 +52,64 @@ except Exception: # pragma: no cover
|
|
|
52
52
|
_core_get_host_vitals = None # type: ignore
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Platform / command shell
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
_IS_WINDOWS = os.name == "nt"
|
|
60
|
+
|
|
61
|
+
#: Hide the console window of every child process on Windows: the node runs
|
|
62
|
+
#: hidden, and a flashing window per command would be the only thing the user sees.
|
|
63
|
+
_POPEN_FLAGS = 0
|
|
64
|
+
if _IS_WINDOWS: # pragma: no cover - Windows only
|
|
65
|
+
_POPEN_FLAGS = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000) | getattr(
|
|
66
|
+
subprocess, "CREATE_NEW_PROCESS_GROUP", 0x00000200)
|
|
67
|
+
|
|
68
|
+
_SHELL_CACHE: Optional[Tuple[str, List[str]]] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _git_bash_windows() -> Optional[str]:
|
|
72
|
+
"""Git for Windows' bash.exe, never System32\\bash.exe.
|
|
73
|
+
|
|
74
|
+
On Windows "bash" on PATH is normally the WSL launcher: it runs the command
|
|
75
|
+
inside a Linux VM (wrong files, wrong processes) or fails without WSL.
|
|
76
|
+
"""
|
|
77
|
+
roots = [os.environ.get(k) for k in ("ProgramFiles", "ProgramW6432", "ProgramFiles(x86)", "LOCALAPPDATA")]
|
|
78
|
+
candidates = []
|
|
79
|
+
for root in filter(None, roots):
|
|
80
|
+
candidates.append(os.path.join(root, "Git", "bin", "bash.exe"))
|
|
81
|
+
candidates.append(os.path.join(root, "Programs", "Git", "bin", "bash.exe"))
|
|
82
|
+
git = shutil.which("git")
|
|
83
|
+
if git: # ...\Git\cmd\git.exe -> ...\Git\bin\bash.exe
|
|
84
|
+
candidates.append(os.path.join(os.path.dirname(os.path.dirname(git)), "bin", "bash.exe"))
|
|
85
|
+
for path in candidates:
|
|
86
|
+
if os.path.isfile(path):
|
|
87
|
+
return path
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def command_shell() -> Tuple[str, List[str]]:
|
|
92
|
+
"""(name, argv prefix) of the shell that runs bash_exec / run_job commands."""
|
|
93
|
+
global _SHELL_CACHE
|
|
94
|
+
if _SHELL_CACHE is None:
|
|
95
|
+
if _IS_WINDOWS:
|
|
96
|
+
bash = _git_bash_windows()
|
|
97
|
+
if bash:
|
|
98
|
+
_SHELL_CACHE = ("git-bash", [bash, "-c"])
|
|
99
|
+
else:
|
|
100
|
+
ps = shutil.which("pwsh") or shutil.which("powershell") or "powershell"
|
|
101
|
+
_SHELL_CACHE = ("powershell", [ps, "-NoLogo", "-NoProfile", "-NonInteractive",
|
|
102
|
+
"-ExecutionPolicy", "Bypass", "-Command"])
|
|
103
|
+
else:
|
|
104
|
+
bash = shutil.which("bash")
|
|
105
|
+
_SHELL_CACHE = ("bash", [bash, "-c"]) if bash else ("sh", ["/bin/sh", "-c"])
|
|
106
|
+
return _SHELL_CACHE
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _shell_argv(command: str) -> List[str]:
|
|
110
|
+
return command_shell()[1] + [command]
|
|
111
|
+
|
|
112
|
+
|
|
55
113
|
# ---------------------------------------------------------------------------
|
|
56
114
|
# Configuration / module state
|
|
57
115
|
# ---------------------------------------------------------------------------
|
|
@@ -472,6 +530,9 @@ def _tool_system_info(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
472
530
|
"desktop": os.environ.get("XDG_CURRENT_DESKTOP") or os.environ.get("DESKTOP_SESSION") or "",
|
|
473
531
|
"session_type": os.environ.get("XDG_SESSION_TYPE", ""),
|
|
474
532
|
"shell": os.environ.get("SHELL", ""),
|
|
533
|
+
# The shell bash_exec/run_job really use: on a Windows node without Git
|
|
534
|
+
# Bash this is PowerShell, and commands must be written for it.
|
|
535
|
+
"command_shell": command_shell()[0],
|
|
475
536
|
"disks": run("df -h --output=target,size,used,avail,pcent 2>/dev/null | head -8"),
|
|
476
537
|
"memory": run("free -h 2>/dev/null | head -3"),
|
|
477
538
|
"load": run("uptime"),
|
|
@@ -573,12 +634,14 @@ def _run_bash(command: str, timeout: int) -> Tuple[str, str, int, float]:
|
|
|
573
634
|
started = time.time()
|
|
574
635
|
try:
|
|
575
636
|
proc = subprocess.run(
|
|
576
|
-
|
|
637
|
+
_shell_argv(command),
|
|
577
638
|
cwd=_WORKSPACE if os.path.isdir(_WORKSPACE) else None,
|
|
639
|
+
stdin=subprocess.DEVNULL,
|
|
578
640
|
capture_output=True,
|
|
579
641
|
text=True,
|
|
580
642
|
timeout=timeout,
|
|
581
643
|
errors="replace",
|
|
644
|
+
creationflags=_POPEN_FLAGS,
|
|
582
645
|
)
|
|
583
646
|
return proc.stdout or "", proc.stderr or "", proc.returncode, round(time.time() - started, 3)
|
|
584
647
|
except subprocess.TimeoutExpired as exc:
|
|
@@ -1024,6 +1087,8 @@ def _pid_alive(pid: Any, start_time: Any = None) -> bool:
|
|
|
1024
1087
|
return False
|
|
1025
1088
|
except (TypeError, ValueError):
|
|
1026
1089
|
pass
|
|
1090
|
+
if _IS_WINDOWS:
|
|
1091
|
+
return _pid_alive_windows(pid_int)
|
|
1027
1092
|
try:
|
|
1028
1093
|
os.kill(pid_int, 0)
|
|
1029
1094
|
except ProcessLookupError:
|
|
@@ -1035,6 +1100,32 @@ def _pid_alive(pid: Any, start_time: Any = None) -> bool:
|
|
|
1035
1100
|
return True
|
|
1036
1101
|
|
|
1037
1102
|
|
|
1103
|
+
def _pid_alive_windows(pid: int) -> bool:
|
|
1104
|
+
"""Liveness probe for Windows.
|
|
1105
|
+
|
|
1106
|
+
os.kill(pid, 0) must never be used there: any signal other than
|
|
1107
|
+
CTRL_C/CTRL_BREAK goes to TerminateProcess, so the "probe" would kill the
|
|
1108
|
+
very job it checks.
|
|
1109
|
+
"""
|
|
1110
|
+
try:
|
|
1111
|
+
import ctypes
|
|
1112
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
1113
|
+
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
1114
|
+
STILL_ACTIVE = 259
|
|
1115
|
+
handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, int(pid))
|
|
1116
|
+
if not handle:
|
|
1117
|
+
return False
|
|
1118
|
+
try:
|
|
1119
|
+
code = ctypes.c_ulong()
|
|
1120
|
+
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
|
|
1121
|
+
return False
|
|
1122
|
+
return code.value == STILL_ACTIVE
|
|
1123
|
+
finally:
|
|
1124
|
+
kernel32.CloseHandle(handle)
|
|
1125
|
+
except Exception:
|
|
1126
|
+
return False
|
|
1127
|
+
|
|
1128
|
+
|
|
1038
1129
|
def _duration_from_meta(meta: Dict[str, Any]) -> Optional[float]:
|
|
1039
1130
|
started = meta.get("started_at")
|
|
1040
1131
|
finished = meta.get("finished_at")
|
|
@@ -1209,13 +1300,14 @@ def _tool_run_job(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1209
1300
|
|
|
1210
1301
|
try:
|
|
1211
1302
|
proc = subprocess.Popen(
|
|
1212
|
-
|
|
1303
|
+
_shell_argv(command),
|
|
1213
1304
|
cwd=cwd,
|
|
1214
1305
|
stdin=subprocess.DEVNULL,
|
|
1215
1306
|
stdout=subprocess.PIPE,
|
|
1216
1307
|
stderr=subprocess.PIPE,
|
|
1217
1308
|
bufsize=0,
|
|
1218
|
-
start_new_session=
|
|
1309
|
+
start_new_session=not _IS_WINDOWS,
|
|
1310
|
+
creationflags=_POPEN_FLAGS,
|
|
1219
1311
|
)
|
|
1220
1312
|
except Exception as exc:
|
|
1221
1313
|
return {"error": "failed to start job: %s" % exc}
|
|
@@ -1338,13 +1430,25 @@ def _tool_job_output(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1338
1430
|
return {"error": "job_output failed: %s" % exc}
|
|
1339
1431
|
|
|
1340
1432
|
|
|
1433
|
+
# Windows' signal module has no SIGKILL/SIGHUP/SIGQUIT: a literal dict of them
|
|
1434
|
+
# raised AttributeError at import, so the whole node failed to start there.
|
|
1341
1435
|
_SIGNALS = {
|
|
1342
|
-
|
|
1343
|
-
"KILL"
|
|
1344
|
-
"
|
|
1345
|
-
"HUP": _signal.SIGHUP,
|
|
1346
|
-
"QUIT": _signal.SIGQUIT,
|
|
1436
|
+
name: getattr(_signal, "SIG" + name)
|
|
1437
|
+
for name in ("TERM", "KILL", "INT", "HUP", "QUIT")
|
|
1438
|
+
if hasattr(_signal, "SIG" + name)
|
|
1347
1439
|
}
|
|
1440
|
+
if _IS_WINDOWS:
|
|
1441
|
+
# Every name is accepted on Windows; the job is ended with a tree kill.
|
|
1442
|
+
for _name in ("TERM", "KILL", "INT", "HUP", "QUIT"):
|
|
1443
|
+
_SIGNALS.setdefault(_name, _signal.SIGTERM)
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
def _kill_tree_windows(pid: int) -> None:
|
|
1447
|
+
"""End a job and its children on Windows (there are no process groups to signal)."""
|
|
1448
|
+
proc = subprocess.run(["taskkill", "/T", "/F", "/PID", str(int(pid))],
|
|
1449
|
+
capture_output=True, text=True, timeout=15)
|
|
1450
|
+
if proc.returncode != 0:
|
|
1451
|
+
raise OSError((proc.stderr or proc.stdout or "taskkill failed").strip())
|
|
1348
1452
|
|
|
1349
1453
|
|
|
1350
1454
|
def _tool_job_kill(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -1373,15 +1477,22 @@ def _tool_job_kill(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1373
1477
|
|
|
1374
1478
|
pid = meta.get("pid")
|
|
1375
1479
|
delivered = False
|
|
1376
|
-
|
|
1377
|
-
os.killpg(os.getpgid(int(pid)), sig)
|
|
1378
|
-
delivered = True
|
|
1379
|
-
except Exception:
|
|
1480
|
+
if _IS_WINDOWS:
|
|
1380
1481
|
try:
|
|
1381
|
-
|
|
1482
|
+
_kill_tree_windows(int(pid))
|
|
1382
1483
|
delivered = True
|
|
1383
1484
|
except Exception as exc:
|
|
1384
|
-
return {"error": "failed to
|
|
1485
|
+
return {"error": "failed to stop job %s: %s" % (job_id, exc)}
|
|
1486
|
+
else:
|
|
1487
|
+
try:
|
|
1488
|
+
os.killpg(os.getpgid(int(pid)), sig)
|
|
1489
|
+
delivered = True
|
|
1490
|
+
except Exception:
|
|
1491
|
+
try:
|
|
1492
|
+
os.kill(int(pid), sig)
|
|
1493
|
+
delivered = True
|
|
1494
|
+
except Exception as exc:
|
|
1495
|
+
return {"error": "failed to signal job %s: %s" % (job_id, exc)}
|
|
1385
1496
|
|
|
1386
1497
|
if delivered:
|
|
1387
1498
|
_update_meta(job_id, status="killed", finished_at=_now_iso(), duration=_duration_from_meta(meta))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gemini-computer-use",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Turn your PC, Laptop or Server into an autonomous execution node for Google Gemini via native MCP: 14 tools, write/edit/search, background jobs, secure reverse tunnel.",
|
|
5
5
|
"main": "bin/cli.js",
|
|
6
6
|
"bin": {
|