arena-bridge 0.0.0-stage → 0.5.1
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/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/README.md +283 -2
- package/SECURITY.md +28 -0
- package/bin/arena-bridge.mjs +7 -0
- package/docs/cover.png +0 -0
- package/package.json +53 -4
- package/src/approvals.mjs +94 -0
- package/src/audit.mjs +27 -0
- package/src/browser.mjs +616 -0
- package/src/cdp.mjs +83 -0
- package/src/cli.mjs +534 -0
- package/src/config.mjs +93 -0
- package/src/device.mjs +62 -0
- package/src/direct.mjs +132 -0
- package/src/engine.mjs +663 -0
- package/src/http.mjs +77 -0
- package/src/hub.mjs +234 -0
- package/src/mac.mjs +563 -0
- package/src/mcp.mjs +93 -0
- package/src/policy.mjs +65 -0
- package/src/prompt.mjs +103 -0
- package/src/selfupdate.mjs +85 -0
- package/src/service.mjs +83 -0
- package/src/trust.mjs +84 -0
- package/src/tunnel.mjs +70 -0
- package/src/ws.mjs +195 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## 0.5.1
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `setup` and `tunnel` now say how to install `cloudflared` when the binary is missing.
|
|
11
|
+
|
|
12
|
+
## 0.5.0
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- macOS desktop control: 13 `mac_*` tools (screen capture with element ids, mouse, keyboard,
|
|
16
|
+
scrolling, drag, applications, windows) on top of the Peekaboo CLI.
|
|
17
|
+
- `arena-bridge setup` — one command from zero to a working bridge: config, machine check,
|
|
18
|
+
API, tunnel and the ready-to-paste prompt.
|
|
19
|
+
- `arena-bridge update` — self-update from the npm registry (`--check` only reports).
|
|
20
|
+
- `arena-bridge service` — run the bridge as a macOS launchd service (`install`/`uninstall`/`status`).
|
|
21
|
+
- `arena-bridge url` and `arena-bridge version` commands.
|
|
22
|
+
- `doctor` now checks Peekaboo, Chrome and the service.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- `list_dir` returned one level of subdirectory content mixed into the top level, which made
|
|
26
|
+
the agent believe folders were missing. It is now top-level only unless `recursive` is set.
|
|
27
|
+
- `list_dir` no longer follows symlinks while listing, so a dead link cannot block it, and it
|
|
28
|
+
reports unreadable directories in `errors` instead of silently skipping them.
|
|
29
|
+
- A blocked call now answers `APPROVAL_PENDING` (with the `approval_id` and what to run)
|
|
30
|
+
instead of `PROTECTED`, so the agent can tell "waiting for a human" from "not allowed".
|
|
31
|
+
- The default approval wait dropped from 90 s to 45 s.
|
|
32
|
+
- `arena-bridge trust` no longer crashes with `trustState is not defined` (Node CLI).
|
|
33
|
+
- The agent token, the device token and the local token are separate; approvals now require the
|
|
34
|
+
local token, so the agent cannot approve its own requests.
|
|
35
|
+
|
|
36
|
+
## 0.4.0
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- Chrome control over CDP with a dependency-free WebSocket/CDP client: 12 `chrome_*` tools,
|
|
40
|
+
an agent-owned browser (headless or visible) and attaching to the user's own Chrome on 9222.
|
|
41
|
+
- Trust windows: `arena-bridge trust --for 2h [--tools ...] [--paths ...]`, local only, and
|
|
42
|
+
it lifts approvals without weakening the path policy.
|
|
43
|
+
- `localToken` gates approval decisions; `~/.arena-bridge` joined the protected paths.
|
|
44
|
+
|
|
45
|
+
## 0.3.0
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- Repeated `--root` flags accumulate instead of overwriting each other; comma lists are split.
|
|
49
|
+
- A friendly message when the port is already in use.
|
|
50
|
+
- The policy is re-read from disk on every call, so changes apply without a restart.
|
|
51
|
+
|
|
52
|
+
## 0.2.0
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
- Cloudflared quick tunnel helper (`arena-bridge tunnel`), HTTP/2 transport by default.
|
|
56
|
+
- Token rotation (`arena-bridge rotate`).
|
|
57
|
+
|
|
58
|
+
## 0.1.0
|
|
59
|
+
|
|
60
|
+
- First release: local HTTP API, MCP endpoint, files, shell, processes, approvals, audit log.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 arena-bridge contributors
|
|
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
CHANGED
|
@@ -1,3 +1,284 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="docs/cover.png" alt="arena-bridge" width="100%">
|
|
3
|
+
</div>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">arena-bridge</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center"><b>Облачный ИИ-агент получает управление вашим компьютером — и только с вашего разрешения.</b></p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/arena-bridge"><img src="https://img.shields.io/npm/v/arena-bridge.svg?style=flat-square" alt="npm"></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/arena-bridge"><img src="https://img.shields.io/npm/dm/arena-bridge.svg?style=flat-square" alt="downloads"></a>
|
|
12
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="license">
|
|
13
|
+
<img src="https://img.shields.io/badge/node-%3E%3D18-green.svg?style=flat-square" alt="node">
|
|
14
|
+
<img src="https://img.shields.io/badge/dependencies-0-brightgreen.svg?style=flat-square" alt="dependencies">
|
|
15
|
+
<img src="https://img.shields.io/badge/tests-92%20passed-brightgreen.svg?style=flat-square" alt="tests">
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<code>npm i -g arena-bridge</code> → <code>arena-bridge setup</code> → вставьте готовый промпт в чат с агентом
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Что это
|
|
25
|
+
|
|
26
|
+
Облачный агент (Arena.ai Agent Mode, Claude, Cursor, любой MCP-клиент) работает в песочнице
|
|
27
|
+
с выходом в интернет, но не имеет доступа к вашему компьютеру. `arena-bridge` — это маленький
|
|
28
|
+
HTTP-сервис на вашей машине: он слушает только localhost, выставляется наружу туннелем
|
|
29
|
+
(cloudflared / ngrok / Tailscale) и отдаёт агенту инструменты, которые вы ему разрешили:
|
|
30
|
+
|
|
31
|
+
* **файлы и шелл** — прочитать, найти, изменить, запустить;
|
|
32
|
+
* **Chrome** — свой браузер агента (headless или с окном) или ваш настоящий Chrome на порту 9222;
|
|
33
|
+
* **macOS-десктоп** — скриншоты, мышь, клавиатура, приложения (через Peekaboo);
|
|
34
|
+
* **процессы** — фоновые дев-серверы с чтением вывода.
|
|
35
|
+
|
|
36
|
+
Всё, что меняет состояние, проходит через гейт подтверждений, а каждое действие пишется в журнал.
|
|
37
|
+
Без вашего `arena-bridge approve` агент ничего не сделает. Наблюдать за машиной можно без
|
|
38
|
+
подтверждений, а работать «без вопросов» — внутри окна доверия, которое вы открываете сами.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Быстрый старт
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 1. установить
|
|
46
|
+
npm i -g arena-bridge
|
|
47
|
+
|
|
48
|
+
# 2. один раз: создать конфиг, проверить машину, поднять API + туннель
|
|
49
|
+
arena-bridge setup --root ~/projects --root ~/Documents
|
|
50
|
+
|
|
51
|
+
# 3. скопировать блок «Paste this into the agent chat» и вставить его в чат с агентом
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`setup` печатает готовый промпт и публичный URL. Дальше — просто говорите агенту задачу.
|
|
55
|
+
|
|
56
|
+
Нужен туннель? Поставьте `cloudflared` (`brew install cloudflared` или бинарник в `/usr/local/bin`).
|
|
57
|
+
Подойдёт и любой другой: `ngrok http 8787`, `tailscale funnel --bg 8787`.
|
|
58
|
+
|
|
59
|
+
### В фоне, чтобы не открывать терминал
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
arena-bridge service install # launchd: стартует вместе с системой, URL всегда живой
|
|
63
|
+
arena-bridge url # текущий публичный URL
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Python
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pipx install arena-bridge # или: pip install arena-bridge
|
|
70
|
+
arena-bridge setup --root ~/projects
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Что агент умеет
|
|
76
|
+
|
|
77
|
+
| Группа | Инструменты | Что это |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| Файлы | `list_dir` `read_file` `write_file` `edit_file` `delete_path` `search_files` `file_get` `file_put` | работа с файлами в разрешённых папках |
|
|
80
|
+
| Шелл | `bash` `sysinfo` | команды и сведения о машине |
|
|
81
|
+
| Процессы | `start_process` `read_process_output` `write_process` `kill_process` | фоновые дев-серверы |
|
|
82
|
+
| Chrome | `chrome_launch` `chrome_attach` `chrome_open` `chrome_navigate` `chrome_snapshot` `chrome_click` `chrome_type` `chrome_press` `chrome_screenshot` `chrome_evaluate` `chrome_tabs` `chrome_stop` | браузер через CDP |
|
|
83
|
+
| Десктоп | `mac_see` `mac_click` `mac_type` `mac_key` `mac_scroll` `mac_move` `mac_drag` `mac_apps` `mac_windows` `mac_app_launch` `mac_app_focus` `mac_app_quit` `mac_permissions` | macOS через Peekaboo |
|
|
84
|
+
|
|
85
|
+
Полный каталог с JSON-схемами: `GET /tools` или `arena-bridge tools`.
|
|
86
|
+
|
|
87
|
+
### Как выбрать инструмент
|
|
88
|
+
|
|
89
|
+
| Агент хочет | Инструмент |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| прочитать страницу | `chrome_snapshot` (`{"mode":"ax"}` — роли и имена, `{"mode":"text"}`, `{"mode":"html"}`) |
|
|
92
|
+
| кликнуть на странице | `chrome_click` (CSS-селектор или видимый текст) |
|
|
93
|
+
| работать в вашем залогиненном Chrome | `chrome_attach` (порт 9222) и те же `chrome_*` |
|
|
94
|
+
| посмотреть на экран или нативное приложение | `mac_see` (скриншот + id кликабельных элементов) |
|
|
95
|
+
| кликнуть в нативном приложении | `mac_click` (id элемента, видимый текст или координаты) |
|
|
96
|
+
| напечатать | `mac_type`, затем `mac_key` для Enter/Tab/cmd+s |
|
|
97
|
+
| посмотреть файлы | `list_dir` (только верхний уровень; глубже — `{"recursive":true}`) |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Управление Chrome
|
|
102
|
+
|
|
103
|
+
Свой минимальный CDP-клиент внутри пакета: **ни puppeteer, ни playwright не нужны**.
|
|
104
|
+
|
|
105
|
+
```jsonc
|
|
106
|
+
// браузер агента: свой профиль, без ваших cookie
|
|
107
|
+
{"tool": "chrome_launch", "args": {"headless": true, "url": "https://example.com"}}
|
|
108
|
+
// прочитать структуру страницы
|
|
109
|
+
{"tool": "chrome_snapshot", "args": {"mode": "ax"}}
|
|
110
|
+
// кликнуть и ввести
|
|
111
|
+
{"tool": "chrome_click", "args": {"text": "Sign in"}}
|
|
112
|
+
{"tool": "chrome_type", "args": {"selector": "input[name=q]", "text": "hello", "submit": true}}
|
|
113
|
+
// скриншот
|
|
114
|
+
{"tool": "chrome_screenshot", "args": {"path": "shot.png"}}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
* `headless: true` — без окна (по умолчанию), `headless: false` — видимое окно;
|
|
118
|
+
* профиль отдельный (`~/.arena-bridge/chrome-profile-*`), в ваши сессии агент не попадает;
|
|
119
|
+
* действия в браузере агента подтверждения не требуют — это его песочница.
|
|
120
|
+
|
|
121
|
+
**Ваш настоящий Chrome** (с вашими логинами) — через `chrome_attach`:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-debug"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```jsonc
|
|
128
|
+
{"tool": "chrome_attach", "args": {"port": 9222}}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Любое действие в присоединённом браузере требует локального подтверждения. Инструментов чтения
|
|
132
|
+
cookies и localStorage нет.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Управление macOS-десктопом
|
|
137
|
+
|
|
138
|
+
Через [Peekaboo](https://github.com/openclaw/Peekaboo) (это же использует и MCP-режим Peekaboo):
|
|
139
|
+
|
|
140
|
+
```jsonc
|
|
141
|
+
{"tool": "mac_see", "args": {"app": "Safari", "mode": "window"}} // скриншот + id элементов
|
|
142
|
+
{"tool": "mac_click", "args": {"on": "B3"}} // клик по id из mac_see
|
|
143
|
+
{"tool": "mac_type", "args": {"text": "hello", "submit": true}}
|
|
144
|
+
{"tool": "mac_key", "args": {"key": "cmd+s"}}
|
|
145
|
+
{"tool": "mac_apps", "args": {}} // что запущено
|
|
146
|
+
{"tool": "mac_permissions", "args": {}} // чего не хватает в macOS
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Установить Peekaboo: `brew install peekaboo` (или бинарник в `/usr/local/bin/peekaboo`).
|
|
150
|
+
macOS спросит разрешения **Screen Recording** и **Accessibility** для терминала, из которого
|
|
151
|
+
запущен мост — без них скриншоты и клики не работают; `mac_permissions` покажет, что не так.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Доверенное окно: работать без подтверждений
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
arena-bridge trust --for 2h # всё, на 2 часа
|
|
159
|
+
arena-bridge trust --for 30m --tools bash # только команды
|
|
160
|
+
arena-bridge trust --for 1h --paths ~/projects/my-app # только эта папка
|
|
161
|
+
arena-bridge trust --status
|
|
162
|
+
arena-bridge trust --off
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
* окно снимает требование **подтверждения**, но не снимает **политику путей**: за пределами
|
|
166
|
+
`roots` и в защищённых каталогах по-прежнему `PROTECTED`;
|
|
167
|
+
* `--paths` ограничивает доверие конкретными папками; `bash` под такое ограничение не попадает —
|
|
168
|
+
чтобы доверять командам, укажите `--tools bash` явно;
|
|
169
|
+
* окно живёт локально (`~/.arena-bridge/trust.json`), агент открыть его не может;
|
|
170
|
+
* после истечения срока подтверждения возвращаются сами, а все действия остаются в журнале.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Безопасность
|
|
175
|
+
|
|
176
|
+
| Токен | Кто знает | Что позволяет |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| `token` | агент | вызывать инструменты по HTTP/MCP |
|
|
179
|
+
| `deviceToken` | только устройство | подключаться к хабу |
|
|
180
|
+
| `localToken` | только вы | подтверждать запросы и открывать окно доверия |
|
|
181
|
+
|
|
182
|
+
* **Агент не может одобрить сам себя**: `POST /approvals/:id` требует локальный токен.
|
|
183
|
+
* **Пути ограничены**: всё вне `roots` — `PROTECTED`. Защищённый список по умолчанию включает
|
|
184
|
+
`~/.ssh`, `~/.aws`, `~/.arena-bridge`, `~/.npmrc`, профили браузеров, ключи.
|
|
185
|
+
* **Подтверждения**: `bash`, запись, удаление, управление вашим Chrome и десктопом ждут
|
|
186
|
+
`arena-bridge approve <id>`. Ответ `APPROVAL_PENDING` содержит `approval_id` — покажите его
|
|
187
|
+
пользователю и повторите вызов один раз.
|
|
188
|
+
* **Журнал**: `~/.arena-bridge/audit.log` (JSONL) — кто, что и когда вызвал.
|
|
189
|
+
* **Транспорт**: сервер слушает `127.0.0.1`, наружу — только туннель с HTTPS.
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
arena-bridge rotate # новый агентский токен (действует мгновенно)
|
|
193
|
+
arena-bridge rotate --device # новый девайсный токен
|
|
194
|
+
arena-bridge audit -n 50 # что происходило
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Команды
|
|
200
|
+
|
|
201
|
+
| Команда | Что делает |
|
|
202
|
+
| --- | --- |
|
|
203
|
+
| `setup` | конфиг + проверка машины + API + туннель + готовый промпт |
|
|
204
|
+
| `init` | создать `~/.arena-bridge/config.json` с токенами (`--root` повторяемый) |
|
|
205
|
+
| `start` | локальный API-сервер |
|
|
206
|
+
| `tunnel` | API + облачный туннель, печатает промпт |
|
|
207
|
+
| `hub` / `connect` | режим «хаб»: VPS принимает подключения многих машин |
|
|
208
|
+
| `prompt` | готовый промпт для чата с агентом |
|
|
209
|
+
| `tools` | каталог инструментов в JSON |
|
|
210
|
+
| `trust` | окно доверия (`--for`, `--tools`, `--paths`, `--off`, `--status`) |
|
|
211
|
+
| `approvals` / `approve <id>` | ожидающие подтверждения и ответ на них (`--deny`) |
|
|
212
|
+
| `audit [-n N]` | журнал действий |
|
|
213
|
+
| `doctor` | проверка установки, токенов, путей, Peekaboo, Chrome |
|
|
214
|
+
| `url` | текущий публичный URL |
|
|
215
|
+
| `version` | установленная и последняя версии |
|
|
216
|
+
| `update` | обновить пакет с npm (`--check` — только проверить) |
|
|
217
|
+
| `service` | macOS-фоновый сервис: `install` / `uninstall` / `status` |
|
|
218
|
+
| `rotate` | сменить агентский или девайсный токен |
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## API и MCP
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
GET /health liveness (без авторизации)
|
|
226
|
+
GET /tools каталог инструментов с JSON-схемами
|
|
227
|
+
GET /prompt этот промпт для агента
|
|
228
|
+
GET /approvals ожидающие подтверждения
|
|
229
|
+
POST /call {"tool":"bash","args":{...}}
|
|
230
|
+
POST /exec {"command":"...","cwd":"...","timeout_sec":120}
|
|
231
|
+
GET|PUT|DELETE /files?path=/abs/path
|
|
232
|
+
POST /approvals/:id {"decision":"approve"|"deny"} — только по локальному токену
|
|
233
|
+
POST /mcp MCP Streamable HTTP (protocolVersion 2025-06-18)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
curl -sS -X POST "https://<ваш-url>/call" \
|
|
238
|
+
-H "Authorization: Bearer $ARENA_BRIDGE_TOKEN" \
|
|
239
|
+
-H "Content-Type: application/json" \
|
|
240
|
+
-d '{"tool":"sysinfo","args":{}}'
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Разработка
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
npm install # зависимостей нет — только dev-тесты
|
|
249
|
+
npm test # 50 тестов Node: direct, approval, trust, MCP, hub, туннель, Chrome, десктоп
|
|
250
|
+
npm run test:python # 42 теста Python: то же самое
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Тесты не требуют ни Chrome, ни Peekaboo: используются фейковые CDP- и Peekaboo-серверы
|
|
254
|
+
(`test/fake-cdp.mjs`, `test/fake-peekaboo.sh`).
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
src/ Node-реализация (zero dependencies)
|
|
258
|
+
ws.mjs минимальный WebSocket-клиент
|
|
259
|
+
cdp.mjs клиент Chrome DevTools Protocol
|
|
260
|
+
browser.mjs 12 инструментов chrome_*
|
|
261
|
+
mac.mjs 13 инструментов mac_* (Peekaboo)
|
|
262
|
+
trust.mjs окно доверия
|
|
263
|
+
policy.mjs проверка путей (roots / protected)
|
|
264
|
+
engine.mjs каталог, гейт подтверждений, аудит
|
|
265
|
+
direct.mjs HTTP-сервер
|
|
266
|
+
hub.mjs хаб для нескольких машин
|
|
267
|
+
cli.mjs команды
|
|
268
|
+
arena_bridge/ тот же функционал на Python (stdlib only)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Частые проблемы
|
|
272
|
+
|
|
273
|
+
| Симптом | Что делать |
|
|
274
|
+
| --- | --- |
|
|
275
|
+
| `port 8787 is already in use` | остановите старый экземпляр (`Ctrl+C`) или `arena-bridge start --port 8788` |
|
|
276
|
+
| туннель не поднимается | `cloudflared` по умолчанию использует QUIC и может быть заблокирован — `arena-bridge tunnel --protocol http2` (это поведение по умолчанию) |
|
|
277
|
+
| агент не видит файлы | проверьте `arena-bridge doctor` и `roots` в `~/.arena-bridge/config.json`; вне `roots` всё равно `PROTECTED` |
|
|
278
|
+
| `APPROVAL_PENDING` | покажите пользователю `arena-bridge approve <id>` или откройте `arena-bridge trust --for 1h` |
|
|
279
|
+
| скриншоты не работают | `mac_permissions` — выдайте терминалу Screen Recording и Accessibility |
|
|
280
|
+
| `peekaboo_not_found` | поставьте Peekaboo или укажите путь: `arena-bridge init` + `peekabooPath` в конфиге |
|
|
281
|
+
|
|
282
|
+
## Лицензия
|
|
283
|
+
|
|
284
|
+
MIT
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Threat model
|
|
4
|
+
|
|
5
|
+
`arena-bridge` gives a remote AI agent access to a machine. The package is designed so that a
|
|
6
|
+
compromise of the agent, of the chat or of the tunnel does not automatically become a compromise
|
|
7
|
+
of the machine.
|
|
8
|
+
|
|
9
|
+
## What protects the machine
|
|
10
|
+
|
|
11
|
+
- **Localhost only.** The server binds `127.0.0.1` by default. Public access requires an explicit
|
|
12
|
+
tunnel.
|
|
13
|
+
- **Three separate tokens.** The agent token can only call tools, the device token only connects
|
|
14
|
+
a device to a hub, and the local token approves requests. The agent never receives the local
|
|
15
|
+
token and cannot approve its own requests.
|
|
16
|
+
- **Path policy.** Every file operation is limited to the configured `roots`. A protected list
|
|
17
|
+
(`~/.ssh`, `~/.aws`, `~/.arena-bridge`, browser profiles, credential files) is denied even
|
|
18
|
+
inside a root.
|
|
19
|
+
- **Approval gate.** Side-effecting tools wait for a human decision. The answer identifies the
|
|
20
|
+
pending approval so the user can approve it with `arena-bridge approve <id>`.
|
|
21
|
+
- **Trust windows are local.** Only the local CLI can open one, and it never weakens the path
|
|
22
|
+
policy — it only removes the approval requirement.
|
|
23
|
+
- **Audit log.** Every call is appended to `~/.arena-bridge/audit.log` (0600) with the tool, the
|
|
24
|
+
arguments and the outcome.
|
|
25
|
+
|
|
26
|
+
## Reporting a vulnerability
|
|
27
|
+
|
|
28
|
+
Please report issues privately to the repository owner instead of opening a public issue.
|
package/docs/cover.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arena-bridge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Give a cloud AI agent (Arena.ai Agent Mode, Claude, Cursor, ...) controlled access to your own machine over HTTP/MCP: files, shell, Chrome and the macOS desktop, with an approval gate, trust windows and an audit log.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"arena-bridge": "bin/arena-bridge.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"docs/cover.png",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"SECURITY.md"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node test/smoke.mjs && node test/browser.test.mjs && node test/mac.test.mjs",
|
|
23
|
+
"test:node": "node test/smoke.mjs && node test/browser.test.mjs && node test/mac.test.mjs",
|
|
24
|
+
"test:python": "python3 test/test_bridge.py && python3 test/test_browser.py && python3 test/test_mac.py",
|
|
25
|
+
"pack:check": "npm pack --dry-run"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/vernikr-personal/arena-bridge.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/vernikr-personal/arena-bridge#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/vernikr-personal/arena-bridge/issues"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"mcp",
|
|
37
|
+
"arena",
|
|
38
|
+
"agent",
|
|
39
|
+
"agent-mode",
|
|
40
|
+
"remote-control",
|
|
41
|
+
"macos",
|
|
42
|
+
"chrome",
|
|
43
|
+
"cdp",
|
|
44
|
+
"peekaboo",
|
|
45
|
+
"automation",
|
|
46
|
+
"sandbox",
|
|
47
|
+
"mcp-server",
|
|
48
|
+
"self-hosted"
|
|
49
|
+
],
|
|
50
|
+
"author": "vernikr-personal",
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { APPROVALS_DIR, ensureDirs } from './config.mjs';
|
|
5
|
+
|
|
6
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
7
|
+
|
|
8
|
+
function fileOf(id) {
|
|
9
|
+
return path.join(APPROVALS_DIR, `${id}.json`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function listApprovals() {
|
|
13
|
+
if (!fs.existsSync(APPROVALS_DIR)) return [];
|
|
14
|
+
return fs
|
|
15
|
+
.readdirSync(APPROVALS_DIR)
|
|
16
|
+
.filter((f) => f.endsWith('.json'))
|
|
17
|
+
.map((f) => {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(path.join(APPROVALS_DIR, f), 'utf8'));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function decide(id, decision, by = 'cli') {
|
|
29
|
+
const file = fileOf(id);
|
|
30
|
+
if (!fs.existsSync(file)) return { ok: false, error: `unknown approval id: ${id}` };
|
|
31
|
+
let rec;
|
|
32
|
+
try {
|
|
33
|
+
rec = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return { ok: false, error: e.message };
|
|
36
|
+
}
|
|
37
|
+
if (rec.status !== 'pending') return { ok: false, error: `already ${rec.status}` };
|
|
38
|
+
rec.status = decision === 'approve' ? 'approved' : 'denied';
|
|
39
|
+
rec.decidedAt = Date.now();
|
|
40
|
+
rec.decidedBy = by;
|
|
41
|
+
fs.writeFileSync(file, JSON.stringify(rec, null, 2), { mode: 0o600 });
|
|
42
|
+
return { ok: true, approval: rec };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function redact(args) {
|
|
46
|
+
const clone = { ...args };
|
|
47
|
+
for (const k of Object.keys(clone)) {
|
|
48
|
+
if (/token|secret|password|key/i.test(k)) clone[k] = '***';
|
|
49
|
+
}
|
|
50
|
+
if (typeof clone.command === 'string' && clone.command.length > 4000) {
|
|
51
|
+
clone.command = clone.command.slice(0, 4000) + '…';
|
|
52
|
+
}
|
|
53
|
+
return clone;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a pending approval request and wait for a local human decision.
|
|
58
|
+
* The request lives in a file so that the `arena-bridge approve` CLI (a separate
|
|
59
|
+
* process) can pick it up.
|
|
60
|
+
*/
|
|
61
|
+
export async function requestApproval(tool, args, cfg) {
|
|
62
|
+
ensureDirs();
|
|
63
|
+
const id = crypto.randomUUID();
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const rec = {
|
|
66
|
+
id,
|
|
67
|
+
tool,
|
|
68
|
+
args: redact(args),
|
|
69
|
+
status: 'pending',
|
|
70
|
+
createdAt: now,
|
|
71
|
+
expiresAt: now + (cfg.approvalTtlSec ?? 900) * 1000,
|
|
72
|
+
decidedAt: null,
|
|
73
|
+
decidedBy: null,
|
|
74
|
+
};
|
|
75
|
+
fs.writeFileSync(fileOf(id), JSON.stringify(rec, null, 2), { mode: 0o600 });
|
|
76
|
+
|
|
77
|
+
const waitSec = Math.min(cfg.approvalWaitSec ?? 90, cfg.approvalTtlSec ?? 900);
|
|
78
|
+
const deadline = now + waitSec * 1000;
|
|
79
|
+
while (Date.now() < deadline) {
|
|
80
|
+
await sleep(400);
|
|
81
|
+
let cur;
|
|
82
|
+
try {
|
|
83
|
+
cur = JSON.parse(fs.readFileSync(fileOf(id), 'utf8'));
|
|
84
|
+
} catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (cur.status === 'approved') return { approved: true, rec: cur };
|
|
88
|
+
if (cur.status === 'denied') return { approved: false, rec: cur };
|
|
89
|
+
if (cur.expiresAt && Date.now() > cur.expiresAt) {
|
|
90
|
+
return { approved: false, rec: { ...cur, status: 'expired' } };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { approved: false, rec: { ...rec, status: 'timeout' } };
|
|
94
|
+
}
|
package/src/audit.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CONFIG_DIR, AUDIT_PATH } from './config.mjs';
|
|
4
|
+
|
|
5
|
+
export function audit(entry) {
|
|
6
|
+
try {
|
|
7
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
8
|
+
const rec = { ts: new Date().toISOString(), ...entry };
|
|
9
|
+
fs.appendFileSync(AUDIT_PATH, JSON.stringify(rec) + '\n', { mode: 0o600 });
|
|
10
|
+
} catch {
|
|
11
|
+
/* auditing must never break the request */
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function tailAudit(n = 50) {
|
|
16
|
+
if (!fs.existsSync(AUDIT_PATH)) return [];
|
|
17
|
+
const lines = fs.readFileSync(AUDIT_PATH, 'utf8').trim().split('\n').filter(Boolean);
|
|
18
|
+
return lines
|
|
19
|
+
.slice(-n)
|
|
20
|
+
.map((l) => {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(l);
|
|
23
|
+
} catch {
|
|
24
|
+
return { raw: l };
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|