arena-bridge 0.6.6 → 0.6.8
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 +17 -0
- package/README.md +35 -1
- package/package.json +1 -1
- package/src/audit.mjs +89 -5
- package/src/browser.mjs +3 -0
- package/src/cli.mjs +33 -3
- package/src/direct.mjs +25 -1
- package/src/engine.mjs +84 -6
- package/src/hub.mjs +1 -1
- package/src/mac.mjs +42 -6
- package/src/mcp.mjs +1 -1
- package/src/prompt.mjs +7 -0
- package/src/service.mjs +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.6.8
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Detailed work log: every call is recorded with its arguments, outcome and duration, and the
|
|
11
|
+
evidence is kept next to the log — screenshots taken on the desktop, the accessibility trees
|
|
12
|
+
that were read, menu trees and the list of Spaces.
|
|
13
|
+
- `audit_log` / `audit_artifact` tools and `GET /audit`, `GET /audit/artifact` endpoints let the
|
|
14
|
+
agent look back at what it saw.
|
|
15
|
+
- `arena-bridge audit` lists the calls, `arena-bridge audit artifacts` lists the saved files,
|
|
16
|
+
`arena-bridge audit <id>` prints one entry, `arena-bridge audit prune` removes old artifacts.
|
|
17
|
+
|
|
18
|
+
## 0.6.7
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- `arena-bridge service install --bin ngrok --domain <name>` installs the macOS background
|
|
22
|
+
service with a fixed public address; `arena-bridge service status` now shows the current URL.
|
|
23
|
+
|
|
7
24
|
## 0.6.6
|
|
8
25
|
|
|
9
26
|
### Fixed
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="license">
|
|
13
13
|
<img src="https://img.shields.io/badge/node-%3E%3D18-green.svg?style=flat-square" alt="node">
|
|
14
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-
|
|
15
|
+
<img src="https://img.shields.io/badge/tests-120%20passed-brightgreen.svg?style=flat-square" alt="tests">
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
@@ -64,6 +64,18 @@ arena-bridge setup --root ~/projects --root ~/Documents
|
|
|
64
64
|
|
|
65
65
|
### В фоне, чтобы не открывать терминал
|
|
66
66
|
|
|
67
|
+
Мост можно поднять как службу macOS — он стартует вместе с системой, переживает закрытие
|
|
68
|
+
терминала и поднимается заново после сбоя:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
arena-bridge service install # облако, случайный адрес
|
|
72
|
+
arena-bridge service install --bin ngrok --domain mymac.ngrok-free.app # постоянный адрес
|
|
73
|
+
arena-bridge service status # состояние и текущий URL
|
|
74
|
+
arena-bridge url # текущий публичный адрес
|
|
75
|
+
arena-bridge service uninstall
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
|
|
67
79
|
```bash
|
|
68
80
|
arena-bridge service install # launchd: стартует вместе с системой, URL всегда живой
|
|
69
81
|
arena-bridge url # текущий публичный URL
|
|
@@ -83,6 +95,7 @@ arena-bridge setup --root ~/projects
|
|
|
83
95
|
| Группа | Инструменты | Что это |
|
|
84
96
|
| --- | --- | --- |
|
|
85
97
|
| Файлы | `list_dir` `read_file` `write_file` `edit_file` `delete_path` `search_files` `file_get` `file_put` | работа с файлами в разрешённых папках |
|
|
98
|
+
| Журнал | `audit_log` `audit_artifact` | что делалось, какие снимки и деревья Accessibility сохранены |
|
|
86
99
|
| Шелл | `bash` `sysinfo` | команды и сведения о машине |
|
|
87
100
|
| Процессы | `start_process` `read_process_output` `write_process` `kill_process` | фоновые дев-серверы |
|
|
88
101
|
| Chrome | `chrome_launch` `chrome_profiles` `chrome_attach` `chrome_open` `chrome_navigate` `chrome_snapshot` `chrome_click` `chrome_type` `chrome_press` `chrome_screenshot` `chrome_evaluate` `chrome_tabs` `chrome_stop` | браузер через CDP |
|
|
@@ -187,6 +200,25 @@ macOS отдаёт приложениям два разных слоя: карт
|
|
|
187
200
|
|
|
188
201
|
---
|
|
189
202
|
|
|
203
|
+
## Журнал работы с артефактами
|
|
204
|
+
|
|
205
|
+
Каждый вызов записывается: аргументы, результат, время выполнения. Рядом с журналом хранятся
|
|
206
|
+
**доказательства** — снимки экрана, деревья Accessibility, деревья меню, список рабочих столов.
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
arena-bridge audit # последние вызовы
|
|
210
|
+
arena-bridge audit artifacts # сохранённые файлы (скриншоты, деревья)
|
|
211
|
+
arena-bridge audit <id> # одна запись целиком
|
|
212
|
+
arena-bridge audit prune # удалить старые артефакты (по умолчанию старше 14 дней)
|
|
213
|
+
arena-bridge audit dir # где всё это лежит
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Агенту доступно то же самое: `audit_log` (вызовы) и `audit_artifact` (сам артефакт — PNG в base64
|
|
217
|
+
или дерево Accessibility в JSON), а также `GET /audit` и `GET /audit/artifact?entry=<id>&file=<name>`.
|
|
218
|
+
Полезно, чтобы оглянуться назад: что было на экране и что находилось в интерфейсе.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
190
222
|
## Доверенное окно: работать без подтверждений
|
|
191
223
|
|
|
192
224
|
```bash
|
|
@@ -297,6 +329,8 @@ GET /health liveness (без авторизации)
|
|
|
297
329
|
GET /tools каталог инструментов с JSON-схемами
|
|
298
330
|
GET /prompt этот промпт для агента
|
|
299
331
|
GET /approvals ожидающие подтверждения
|
|
332
|
+
GET /audit журнал работы с артефактами
|
|
333
|
+
GET /audit/artifact сохранённый артефакт (entry=, file=)
|
|
300
334
|
POST /call {"tool":"bash","args":{...}}
|
|
301
335
|
POST /exec {"command":"...","cwd":"...","timeout_sec":120}
|
|
302
336
|
GET|PUT|DELETE /files?path=/abs/path
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arena-bridge",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/audit.mjs
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { CONFIG_DIR
|
|
3
|
+
import { CONFIG_DIR } from './config.mjs';
|
|
4
|
+
|
|
5
|
+
/** The config directory is resolved on every call so tests and relocations stay isolated. */
|
|
6
|
+
function configDir() {
|
|
7
|
+
return process.env.ARENA_BRIDGE_HOME || CONFIG_DIR;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function auditPath() {
|
|
11
|
+
return path.join(configDir(), 'audit.log');
|
|
12
|
+
}
|
|
4
13
|
|
|
5
14
|
export function audit(entry) {
|
|
6
15
|
try {
|
|
7
|
-
|
|
16
|
+
const dir = configDir();
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
8
18
|
const rec = { ts: new Date().toISOString(), ...entry };
|
|
9
|
-
fs.appendFileSync(
|
|
19
|
+
fs.appendFileSync(auditPath(), JSON.stringify(rec) + '\n', { mode: 0o600 });
|
|
10
20
|
} catch {
|
|
11
21
|
/* auditing must never break the request */
|
|
12
22
|
}
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
export function tailAudit(n = 50) {
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
const file = auditPath();
|
|
27
|
+
if (!fs.existsSync(file)) return [];
|
|
28
|
+
const lines = fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean);
|
|
18
29
|
return lines
|
|
19
30
|
.slice(-n)
|
|
20
31
|
.map((l) => {
|
|
@@ -25,3 +36,76 @@ export function tailAudit(n = 50) {
|
|
|
25
36
|
}
|
|
26
37
|
});
|
|
27
38
|
}
|
|
39
|
+
|
|
40
|
+
/* ------------------------------------------------------------------ *
|
|
41
|
+
* artifacts: screenshots, accessibility trees and other evidence
|
|
42
|
+
* ------------------------------------------------------------------ */
|
|
43
|
+
|
|
44
|
+
export function auditDir() {
|
|
45
|
+
return path.join(configDir(), 'audit');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function saveArtifact(entryId, kind, name, data, meta = {}) {
|
|
49
|
+
try {
|
|
50
|
+
const dir = path.join(auditDir(), String(entryId));
|
|
51
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
52
|
+
const file = path.join(dir, path.basename(String(name)));
|
|
53
|
+
fs.writeFileSync(file, data, { mode: 0o600 });
|
|
54
|
+
const bytes = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(String(data));
|
|
55
|
+
return { kind, name: path.basename(String(name)), path: file, bytes, ...meta };
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function readArtifact(entryId, name) {
|
|
62
|
+
try {
|
|
63
|
+
const file = path.join(auditDir(), String(entryId), path.basename(String(name)));
|
|
64
|
+
if (!fs.existsSync(file)) return null;
|
|
65
|
+
return { file, data: fs.readFileSync(file) };
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function listArtifacts() {
|
|
72
|
+
const dir = auditDir();
|
|
73
|
+
if (!fs.existsSync(dir)) return [];
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
76
|
+
const sub = path.join(dir, entry);
|
|
77
|
+
let files = [];
|
|
78
|
+
try {
|
|
79
|
+
files = fs
|
|
80
|
+
.readdirSync(sub)
|
|
81
|
+
.filter((f) => fs.statSync(path.join(sub, f)).isFile())
|
|
82
|
+
.map((f) => ({ name: f, bytes: fs.statSync(path.join(sub, f)).size }));
|
|
83
|
+
} catch {
|
|
84
|
+
/* ignore */
|
|
85
|
+
}
|
|
86
|
+
if (files.length) out.push({ entry, files });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function pruneArtifacts(days = 14) {
|
|
92
|
+
const dir = auditDir();
|
|
93
|
+
if (!fs.existsSync(dir)) return { removed: 0, freed_bytes: 0 };
|
|
94
|
+
const cutoff = Date.now() - Number(days) * 86400000;
|
|
95
|
+
let removed = 0;
|
|
96
|
+
let freed = 0;
|
|
97
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
98
|
+
const sub = path.join(dir, entry);
|
|
99
|
+
try {
|
|
100
|
+
const st = fs.statSync(sub);
|
|
101
|
+
if (st.mtimeMs < cutoff) {
|
|
102
|
+
freed += st.size;
|
|
103
|
+
fs.rmSync(sub, { recursive: true, force: true });
|
|
104
|
+
removed++;
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
/* ignore */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { removed, freed_bytes: freed };
|
|
111
|
+
}
|
package/src/browser.mjs
CHANGED
|
@@ -652,6 +652,9 @@ export class BrowserManager {
|
|
|
652
652
|
}
|
|
653
653
|
const { data } = await session.cdp.send('Page.captureScreenshot', params, tab.sessionId);
|
|
654
654
|
const b64 = String(data || '');
|
|
655
|
+
if (ctx.artifact && b64) {
|
|
656
|
+
ctx.artifact('screenshot', 'page.png', Buffer.from(b64, 'base64'), { source: 'chrome_screenshot' });
|
|
657
|
+
}
|
|
655
658
|
if (args.path) {
|
|
656
659
|
const chk = checkPath(args.path, ctx.cfg, ctx.base, { write: true });
|
|
657
660
|
if (!chk.ok) return { ok: false, error: chk.error, reason: chk.reason };
|
package/src/cli.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { createHubServer } from './hub.mjs';
|
|
|
9
9
|
import { runDevice } from './device.mjs';
|
|
10
10
|
import { buildPrompt } from './prompt.mjs';
|
|
11
11
|
import { listApprovals, decide } from './approvals.mjs';
|
|
12
|
-
import { tailAudit } from './audit.mjs';
|
|
12
|
+
import { tailAudit, auditDir, listArtifacts, pruneArtifacts } from './audit.mjs';
|
|
13
13
|
import { startTunnel, isPortOpen } from './tunnel.mjs';
|
|
14
14
|
import { spawn, execSync } from 'node:child_process';
|
|
15
15
|
import { parseDuration, trustState, saveTrust, clearTrust } from './trust.mjs';
|
|
@@ -377,10 +377,33 @@ function cmdTrust(args) {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
function cmdAudit(args) {
|
|
380
|
+
const what = args._[1];
|
|
381
|
+
if (what === 'dir') {
|
|
382
|
+
console.log(auditDir());
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (what === 'prune') {
|
|
386
|
+
const res = pruneArtifacts(Number(args.flags.days || 14));
|
|
387
|
+
console.log(`removed ${res.removed} artifact folder(s), ${res.freed_bytes} bytes`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (what === 'artifacts') {
|
|
391
|
+
for (const item of listArtifacts()) {
|
|
392
|
+
const files = item.files.map((f) => `${f.name} (${f.bytes} B)`).join(', ');
|
|
393
|
+
console.log(`${item.entry} ${files}`);
|
|
394
|
+
}
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (what && what.length > 6) {
|
|
398
|
+
const rec = tailAudit(500).find((e) => e.id === what);
|
|
399
|
+
console.log(rec ? JSON.stringify(rec, null, 2) : `no entry with id ${what}`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
380
402
|
const n = Number(args.flags.n || 30);
|
|
381
403
|
for (const rec of tailAudit(n)) {
|
|
404
|
+
const arts = (rec.artifacts || []).map((a) => a.name).join(' ');
|
|
382
405
|
console.log(
|
|
383
|
-
`${rec.ts} ${String(rec.tool).padEnd(18)} ${String(rec.status).padEnd(
|
|
406
|
+
`${rec.ts} ${String(rec.tool).padEnd(18)} ${String(rec.status).padEnd(14)} ${String(rec.duration_ms ?? '').padStart(6)}ms${arts ? ' ' + arts : ''}${
|
|
384
407
|
rec.error ? ' ' + rec.error : ''
|
|
385
408
|
}`,
|
|
386
409
|
);
|
|
@@ -440,7 +463,12 @@ async function cmdUpdate(args) {
|
|
|
440
463
|
function cmdService(args) {
|
|
441
464
|
const what = args._[1] || 'status';
|
|
442
465
|
if (what === 'install') {
|
|
443
|
-
const res = serviceInstall(
|
|
466
|
+
const res = serviceInstall({
|
|
467
|
+
bin: args.flags.bin,
|
|
468
|
+
domain: args.flags.domain,
|
|
469
|
+
authtoken: args.flags.authtoken,
|
|
470
|
+
port: args.flags.port,
|
|
471
|
+
});
|
|
444
472
|
console.log(`\u2713 service installed: ${res.plist}`);
|
|
445
473
|
console.log(' the bridge now starts with the system and keeps a public URL alive');
|
|
446
474
|
console.log(' current URL: arena-bridge url');
|
|
@@ -454,6 +482,8 @@ function cmdService(args) {
|
|
|
454
482
|
const st = serviceStatus();
|
|
455
483
|
console.log(`service: ${st.installed ? 'installed' : 'not installed'} (${st.running ? 'running' : 'not running'})`);
|
|
456
484
|
console.log(` plist: ${st.plist}`);
|
|
485
|
+
const current = loadConfig().publicUrl;
|
|
486
|
+
if (current) console.log(` current URL: ${current}`);
|
|
457
487
|
if (st.detail) console.log(` ${st.detail.split('\n')[0]}`);
|
|
458
488
|
if (!st.installed) console.log(' install it with: arena-bridge service install');
|
|
459
489
|
}
|
package/src/direct.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { buildPrompt } from './prompt.mjs';
|
|
|
6
6
|
import { listApprovals, decide } from './approvals.mjs';
|
|
7
7
|
import { checkPath } from './policy.mjs';
|
|
8
8
|
import { loadConfig } from './config.mjs';
|
|
9
|
+
import { tailAudit, readArtifact, auditDir } from './audit.mjs';
|
|
9
10
|
|
|
10
11
|
export function createDirectServer(cfg, engine) {
|
|
11
12
|
const mcpCtx = {
|
|
@@ -29,7 +30,7 @@ export function createDirectServer(cfg, engine) {
|
|
|
29
30
|
ok: true,
|
|
30
31
|
mode: 'direct',
|
|
31
32
|
device: cfg.device,
|
|
32
|
-
version: '0.6.
|
|
33
|
+
version: '0.6.8',
|
|
33
34
|
uptime_sec: Math.round(process.uptime()),
|
|
34
35
|
});
|
|
35
36
|
}
|
|
@@ -62,6 +63,29 @@ export function createDirectServer(cfg, engine) {
|
|
|
62
63
|
});
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
if (req.method === 'GET' && p === '/audit') {
|
|
67
|
+
const limit = Math.min(200, Number(url.searchParams.get('limit')) || 30);
|
|
68
|
+
return sendJson(res, 200, { ok: true, entries: tailAudit(limit), artifacts_dir: auditDir() });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (req.method === 'GET' && p === '/audit/artifact') {
|
|
72
|
+
const entry = url.searchParams.get('entry');
|
|
73
|
+
const file = url.searchParams.get('file');
|
|
74
|
+
if (!entry || !file) {
|
|
75
|
+
return sendJson(res, 400, { ok: false, error: 'entry and file query parameters are required' });
|
|
76
|
+
}
|
|
77
|
+
const art = readArtifact(entry, file);
|
|
78
|
+
if (!art) return sendJson(res, 404, { ok: false, error: 'artifact_not_found' });
|
|
79
|
+
const type = /\.png$/i.test(art.file)
|
|
80
|
+
? 'image/png'
|
|
81
|
+
: /\.jpe?g$/i.test(art.file)
|
|
82
|
+
? 'image/jpeg'
|
|
83
|
+
: /\.json$/i.test(art.file)
|
|
84
|
+
? 'application/json; charset=utf-8'
|
|
85
|
+
: 'text/plain; charset=utf-8';
|
|
86
|
+
return sendRaw(res, 200, art.data, type);
|
|
87
|
+
}
|
|
88
|
+
|
|
65
89
|
if (req.method === 'GET' && p === '/approvals') {
|
|
66
90
|
return sendJson(res, 200, { ok: true, approvals: listApprovals() });
|
|
67
91
|
}
|
package/src/engine.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { checkPath, resolveTarget, isInside } from './policy.mjs';
|
|
7
|
-
import { audit } from './audit.mjs';
|
|
7
|
+
import { audit, saveArtifact, tailAudit, readArtifact, auditDir } from './audit.mjs';
|
|
8
8
|
import { requestApproval } from './approvals.mjs';
|
|
9
9
|
import { loadConfig, CONFIG_PATH } from './config.mjs';
|
|
10
10
|
import { BrowserManager } from './browser.mjs';
|
|
@@ -393,6 +393,63 @@ export function toolCatalog() {
|
|
|
393
393
|
return { ok: true, path: chk.abs, bytes: data.length };
|
|
394
394
|
},
|
|
395
395
|
},
|
|
396
|
+
{
|
|
397
|
+
name: 'audit_log',
|
|
398
|
+
description:
|
|
399
|
+
'Read the detailed log of what happened on this machine: every tool call with its arguments, outcome and saved artifacts (screenshots, accessibility trees). Read-only.',
|
|
400
|
+
approval: false,
|
|
401
|
+
schema: S(
|
|
402
|
+
{
|
|
403
|
+
limit: { type: 'number', description: 'how many entries to return, default 30' },
|
|
404
|
+
tool: { type: 'string', description: 'only entries of this tool' },
|
|
405
|
+
since: { type: 'string', description: 'only entries newer than this ISO timestamp' },
|
|
406
|
+
},
|
|
407
|
+
[],
|
|
408
|
+
),
|
|
409
|
+
required: [],
|
|
410
|
+
handler: async (args) => {
|
|
411
|
+
const n = Math.min(200, Number(args.limit) || 30);
|
|
412
|
+
let entries = tailAudit(n * 4);
|
|
413
|
+
if (args.tool) entries = entries.filter((e) => e.tool === args.tool);
|
|
414
|
+
if (args.since) {
|
|
415
|
+
const since = Date.parse(String(args.since));
|
|
416
|
+
if (!Number.isNaN(since)) entries = entries.filter((e) => Date.parse(e.ts) >= since);
|
|
417
|
+
}
|
|
418
|
+
entries = entries.slice(-n);
|
|
419
|
+
return { ok: true, count: entries.length, entries, artifacts_dir: auditDir() };
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: 'audit_artifact',
|
|
424
|
+
description:
|
|
425
|
+
'Fetch a saved artifact of a logged call: a screenshot (PNG as base64) or an accessibility tree (JSON). Read-only.',
|
|
426
|
+
approval: false,
|
|
427
|
+
schema: S(
|
|
428
|
+
{
|
|
429
|
+
entry: { type: 'string', description: 'entry id from audit_log' },
|
|
430
|
+
file: { type: 'string', description: 'artifact file name, e.g. "screen.png" or "ax-tree.json"' },
|
|
431
|
+
},
|
|
432
|
+
['entry', 'file'],
|
|
433
|
+
),
|
|
434
|
+
required: ['entry', 'file'],
|
|
435
|
+
handler: async (args) => {
|
|
436
|
+
const art = readArtifact(args.entry, args.file);
|
|
437
|
+
if (!art) {
|
|
438
|
+
return { ok: false, error: 'artifact_not_found', hint: 'Run audit_log to see the available artifacts.' };
|
|
439
|
+
}
|
|
440
|
+
if (/\.(png|jpe?g)$/i.test(art.file)) {
|
|
441
|
+
return { ok: true, kind: 'image', file: art.file, bytes: art.data.length, image_base64: art.data.toString('base64') };
|
|
442
|
+
}
|
|
443
|
+
const text = art.data.toString('utf8');
|
|
444
|
+
let json = null;
|
|
445
|
+
try {
|
|
446
|
+
json = JSON.parse(text);
|
|
447
|
+
} catch {
|
|
448
|
+
/* not json */
|
|
449
|
+
}
|
|
450
|
+
return { ok: true, kind: json ? 'json' : 'text', file: art.file, bytes: art.data.length, content: json ?? text.slice(0, 200000) };
|
|
451
|
+
},
|
|
452
|
+
},
|
|
396
453
|
{
|
|
397
454
|
name: 'sysinfo',
|
|
398
455
|
description: 'Read-only information about the remote machine: OS, user, uptime, memory, disks, network interfaces.',
|
|
@@ -581,6 +638,17 @@ export class Engine {
|
|
|
581
638
|
if (!tool) return { ok: false, error: `unknown tool: ${name}` };
|
|
582
639
|
const started = Date.now();
|
|
583
640
|
let approval = null;
|
|
641
|
+
const entryId = `${new Date().toISOString().replace(/[:.]/g, '-')}-${String(name).replace(/[^\w.-]/g, '_')}-${Math.random()
|
|
642
|
+
.toString(36)
|
|
643
|
+
.slice(2, 8)}`;
|
|
644
|
+
const ctx = Object.create(this);
|
|
645
|
+
ctx.entryId = entryId;
|
|
646
|
+
ctx.artifacts = [];
|
|
647
|
+
ctx.artifact = (kind, file, data, meta) => {
|
|
648
|
+
const saved = saveArtifact(entryId, kind, file, data, meta);
|
|
649
|
+
if (saved) ctx.artifacts.push(saved);
|
|
650
|
+
return saved;
|
|
651
|
+
};
|
|
584
652
|
|
|
585
653
|
// pick up policy changes (roots, protected, requireApproval) without a restart
|
|
586
654
|
try {
|
|
@@ -598,16 +666,16 @@ export class Engine {
|
|
|
598
666
|
const trust = trustState();
|
|
599
667
|
if (trustAllows(trust, tool.name, args, this.cfg)) {
|
|
600
668
|
if (this.cfg.audit !== false) {
|
|
601
|
-
audit({ tool: tool.name, args, status: 'ok', via: 'trust_window' });
|
|
669
|
+
audit({ id: entryId, tool: tool.name, args, status: 'ok', via: 'trust_window' });
|
|
602
670
|
}
|
|
603
|
-
const res = await tool.handler(args,
|
|
671
|
+
const res = await tool.handler(args, ctx);
|
|
604
672
|
return res;
|
|
605
673
|
}
|
|
606
674
|
approval = await requestApproval(tool.name, args, this.cfg);
|
|
607
675
|
if (!approval.approved) {
|
|
608
676
|
const status = approval.rec?.status || 'denied';
|
|
609
677
|
if (this.cfg.audit !== false) {
|
|
610
|
-
audit({ tool: tool.name, args, status: 'approval_denied', approval_status: status });
|
|
678
|
+
audit({ id: entryId, tool: tool.name, args, status: 'approval_denied', approval_status: status });
|
|
611
679
|
}
|
|
612
680
|
const pending = status === 'pending' || status === 'timeout' || status === 'expired';
|
|
613
681
|
return {
|
|
@@ -627,21 +695,31 @@ export class Engine {
|
|
|
627
695
|
}
|
|
628
696
|
|
|
629
697
|
try {
|
|
630
|
-
const res = await tool.handler(args,
|
|
698
|
+
const res = await tool.handler(args, ctx);
|
|
631
699
|
if (this.cfg.audit !== false) {
|
|
632
700
|
audit({
|
|
701
|
+
id: entryId,
|
|
633
702
|
tool: tool.name,
|
|
634
703
|
args,
|
|
635
704
|
status: res.ok ? 'ok' : 'error',
|
|
636
705
|
duration_ms: Date.now() - started,
|
|
637
706
|
exit_code: res.exit_code,
|
|
638
707
|
error: res.ok ? undefined : res.error,
|
|
708
|
+
artifacts: ctx.artifacts.length ? ctx.artifacts : undefined,
|
|
639
709
|
});
|
|
640
710
|
}
|
|
641
711
|
return res;
|
|
642
712
|
} catch (e) {
|
|
643
713
|
if (this.cfg.audit !== false) {
|
|
644
|
-
audit({
|
|
714
|
+
audit({
|
|
715
|
+
id: entryId,
|
|
716
|
+
tool: tool.name,
|
|
717
|
+
args,
|
|
718
|
+
status: 'exception',
|
|
719
|
+
duration_ms: Date.now() - started,
|
|
720
|
+
error: e.message,
|
|
721
|
+
artifacts: ctx.artifacts.length ? ctx.artifacts : undefined,
|
|
722
|
+
});
|
|
645
723
|
}
|
|
646
724
|
return { ok: false, error: e.message };
|
|
647
725
|
}
|
package/src/hub.mjs
CHANGED
package/src/mac.mjs
CHANGED
|
@@ -284,6 +284,21 @@ export function macTools(cfg = {}) {
|
|
|
284
284
|
out.hint += ' No elements found — run mac_permissions: the element list comes from the macOS Accessibility API.';
|
|
285
285
|
}
|
|
286
286
|
if (args.include_raw && seen.json) out.raw = seen.json;
|
|
287
|
+
if (ctx.artifact) {
|
|
288
|
+
try {
|
|
289
|
+
if (fs.existsSync(outPath.abs)) {
|
|
290
|
+
ctx.artifact('screenshot', 'screen.png', fs.readFileSync(outPath.abs), { source: 'mac_see' });
|
|
291
|
+
}
|
|
292
|
+
if (seen.json) {
|
|
293
|
+
ctx.artifact('ax-tree', 'ax-tree.json', JSON.stringify(seen.json, null, 2), {
|
|
294
|
+
source: 'mac_see',
|
|
295
|
+
elements: seen.elements.length,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
/* artifacts are best effort */
|
|
300
|
+
}
|
|
301
|
+
}
|
|
287
302
|
return out;
|
|
288
303
|
},
|
|
289
304
|
},
|
|
@@ -593,11 +608,18 @@ export function macTools(cfg = {}) {
|
|
|
593
608
|
[],
|
|
594
609
|
),
|
|
595
610
|
required: [],
|
|
596
|
-
handler: async (args) => {
|
|
611
|
+
handler: async (args, ctx) => {
|
|
597
612
|
const p = checkPeek(peek());
|
|
598
613
|
if (!p.ok) return p;
|
|
599
614
|
const seen = await searchSee(args);
|
|
600
615
|
const matches = seen.elements.filter((e) => matchElement(e, args)).slice(0, Number(args.limit) || 25);
|
|
616
|
+
if (ctx.artifact && seen.json) {
|
|
617
|
+
ctx.artifact('ax-tree', 'ax-tree.json', JSON.stringify(seen.json, null, 2), {
|
|
618
|
+
source: 'mac_element',
|
|
619
|
+
elements: seen.elements.length,
|
|
620
|
+
query: args.name || args.role || null,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
601
623
|
return {
|
|
602
624
|
ok: true,
|
|
603
625
|
matches,
|
|
@@ -645,7 +667,7 @@ export function macTools(cfg = {}) {
|
|
|
645
667
|
approval: false,
|
|
646
668
|
schema: S({ app: { type: 'string' } }, []),
|
|
647
669
|
required: [],
|
|
648
|
-
handler: async (args) => {
|
|
670
|
+
handler: async (args, ctx) => {
|
|
649
671
|
const p = checkPeek(peek());
|
|
650
672
|
if (!p.ok) return p;
|
|
651
673
|
const flags = menuFlags(args);
|
|
@@ -659,6 +681,9 @@ export function macTools(cfg = {}) {
|
|
|
659
681
|
{ timeout: 30 },
|
|
660
682
|
);
|
|
661
683
|
const json = parseJson(res.stdout);
|
|
684
|
+
if (ctx.artifact && json) {
|
|
685
|
+
ctx.artifact('menu-tree', 'menu-tree.json', JSON.stringify(json, null, 2), { source: 'mac_menu' });
|
|
686
|
+
}
|
|
662
687
|
return {
|
|
663
688
|
ok: res.exit_code === 0,
|
|
664
689
|
menus: json ? (json.menus || json.data?.menus || json.data?.menu_structure || json) : undefined,
|
|
@@ -716,7 +741,7 @@ export function macTools(cfg = {}) {
|
|
|
716
741
|
approval: false,
|
|
717
742
|
schema: S({}, []),
|
|
718
743
|
required: [],
|
|
719
|
-
handler: async () => {
|
|
744
|
+
handler: async (args, ctx) => {
|
|
720
745
|
const p = checkPeek(peek());
|
|
721
746
|
if (!p.ok) return p;
|
|
722
747
|
const res = await runVariants(
|
|
@@ -729,6 +754,9 @@ export function macTools(cfg = {}) {
|
|
|
729
754
|
{ timeout: 30 },
|
|
730
755
|
);
|
|
731
756
|
const json = parseJson(res.stdout);
|
|
757
|
+
if (ctx.artifact && json) {
|
|
758
|
+
ctx.artifact('spaces', 'spaces.json', JSON.stringify(json, null, 2), { source: 'mac_spaces' });
|
|
759
|
+
}
|
|
732
760
|
return {
|
|
733
761
|
ok: res.exit_code === 0,
|
|
734
762
|
spaces: json ? (json.spaces || json.data?.spaces || json) : undefined,
|
|
@@ -744,7 +772,7 @@ export function macTools(cfg = {}) {
|
|
|
744
772
|
approval: true,
|
|
745
773
|
schema: S({ to: { type: 'string', description: 'space id or index from mac_spaces' } }, ['to']),
|
|
746
774
|
required: ['to'],
|
|
747
|
-
handler: async (args) => {
|
|
775
|
+
handler: async (args, ctx) => {
|
|
748
776
|
const p = checkPeek(peek());
|
|
749
777
|
if (!p.ok) return p;
|
|
750
778
|
const res = await runVariants(
|
|
@@ -775,14 +803,16 @@ export function macTools(cfg = {}) {
|
|
|
775
803
|
[],
|
|
776
804
|
),
|
|
777
805
|
required: [],
|
|
778
|
-
handler: async (args) => {
|
|
806
|
+
handler: async (args, ctx) => {
|
|
779
807
|
const p = checkPeek(peek());
|
|
780
808
|
if (!p.ok) return p;
|
|
781
809
|
const limit = Math.min(120, Math.max(1, Number(args.timeout) || 20));
|
|
782
810
|
const deadline = Date.now() + limit * 1000;
|
|
783
811
|
let scanned = 0;
|
|
812
|
+
let last = null;
|
|
784
813
|
while (Date.now() < deadline) {
|
|
785
814
|
const seen = await searchSee(args);
|
|
815
|
+
last = seen;
|
|
786
816
|
scanned = seen.elements.length;
|
|
787
817
|
const hit = seen.elements.find((e) => matchElement(e, args));
|
|
788
818
|
if (args.disappear) {
|
|
@@ -792,6 +822,12 @@ export function macTools(cfg = {}) {
|
|
|
792
822
|
}
|
|
793
823
|
await new Promise((r) => setTimeout(r, 2000));
|
|
794
824
|
}
|
|
825
|
+
if (ctx.artifact && last?.json) {
|
|
826
|
+
ctx.artifact('ax-tree', 'ax-tree.json', JSON.stringify(last.json, null, 2), {
|
|
827
|
+
source: 'mac_wait',
|
|
828
|
+
elements: scanned,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
795
831
|
return {
|
|
796
832
|
ok: false,
|
|
797
833
|
found: false,
|
|
@@ -808,7 +844,7 @@ export function macTools(cfg = {}) {
|
|
|
808
844
|
approval: false,
|
|
809
845
|
schema: S({}, []),
|
|
810
846
|
required: [],
|
|
811
|
-
handler: async () => {
|
|
847
|
+
handler: async (args, ctx) => {
|
|
812
848
|
const p = checkPeek(peek());
|
|
813
849
|
if (!p.ok) {
|
|
814
850
|
return {
|
package/src/mcp.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export async function handleMcp(req, res, ctx) {
|
|
|
48
48
|
mcpResponse(id, {
|
|
49
49
|
protocolVersion: PROTOCOL_VERSION,
|
|
50
50
|
capabilities: { tools: { listChanged: false } },
|
|
51
|
-
serverInfo: { name: 'arena-bridge', version: '0.6.
|
|
51
|
+
serverInfo: { name: 'arena-bridge', version: '0.6.8' },
|
|
52
52
|
instructions:
|
|
53
53
|
'Tools operate the user\'s own machine. Dangerous operations may return ' +
|
|
54
54
|
'PROTECTED and require a local human approval.',
|
package/src/prompt.mjs
CHANGED
|
@@ -97,6 +97,13 @@ ${tools.join('\n')}
|
|
|
97
97
|
9. \`mac_spaces\` lists the macOS virtual desktops; \`mac_space_switch\` moves the agent to another
|
|
98
98
|
Space so the user keeps the screen they are looking at.
|
|
99
99
|
|
|
100
|
+
## Work log with evidence
|
|
101
|
+
Every call is logged with its arguments, result and duration. Screenshots taken on the desktop and
|
|
102
|
+
the accessibility trees that were read are stored next to the log.
|
|
103
|
+
- \`audit_log\` — the recent calls (filter by tool or time).
|
|
104
|
+
- \`audit_artifact\` — fetch a saved artifact: a screenshot (base64 PNG) or an accessibility tree (JSON).
|
|
105
|
+
Use them when you need to look back at what was on the screen or what the interface contained.
|
|
106
|
+
|
|
100
107
|
## Approvals
|
|
101
108
|
${trustLine}
|
|
102
109
|
- When a call answers \`APPROVAL_PENDING\` it carries an \`approval_id\`: tell the user to run
|
package/src/service.mjs
CHANGED
|
@@ -21,9 +21,14 @@ function cliEntry() {
|
|
|
21
21
|
return path.join(here, '..', 'bin', 'arena-bridge.mjs');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function plistXml() {
|
|
24
|
+
function plistXml({ bin, domain, authtoken, port } = {}) {
|
|
25
25
|
const cli = cliEntry();
|
|
26
26
|
const logs = logDir();
|
|
27
|
+
const extra = [];
|
|
28
|
+
if (bin) extra.push('--bin', String(bin));
|
|
29
|
+
if (domain) extra.push('--domain', String(domain));
|
|
30
|
+
if (authtoken) extra.push('--authtoken', String(authtoken));
|
|
31
|
+
if (port) extra.push('--port', String(port));
|
|
27
32
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
28
33
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
29
34
|
<plist version="1.0">
|
|
@@ -34,6 +39,7 @@ function plistXml() {
|
|
|
34
39
|
<string>${process.execPath}</string>
|
|
35
40
|
<string>${cli}</string>
|
|
36
41
|
<string>tunnel</string>
|
|
42
|
+
${extra.map((a) => ` <string>${a}</string>`).join('\n')}
|
|
37
43
|
</array>
|
|
38
44
|
<key>RunAtLoad</key><true/>
|
|
39
45
|
<key>KeepAlive</key><true/>
|
|
@@ -63,12 +69,12 @@ export function serviceStatus() {
|
|
|
63
69
|
return { installed, running, plist: plistPath(), detail: out.trim().slice(0, 600) };
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
export function serviceInstall() {
|
|
72
|
+
export function serviceInstall(options = {}) {
|
|
67
73
|
ensureDirs();
|
|
68
74
|
fs.mkdirSync(logDir(), { recursive: true });
|
|
69
75
|
const dir = path.dirname(plistPath());
|
|
70
76
|
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
-
fs.writeFileSync(plistPath(), plistXml());
|
|
77
|
+
fs.writeFileSync(plistPath(), plistXml(options));
|
|
72
78
|
launchctl(['bootout', `gui/${process.getuid()}/${LABEL}`], { quiet: true });
|
|
73
79
|
const out = launchctl(['bootstrap', `gui/${process.getuid()}`, plistPath()], { quiet: true });
|
|
74
80
|
launchctl(['enable', `gui/${process.getuid()}/${LABEL}`], { quiet: true });
|