arena-bridge 0.6.7 → 0.6.9
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 +19 -0
- package/README.md +23 -1
- package/package.json +1 -1
- package/src/audit.mjs +89 -5
- package/src/browser.mjs +3 -0
- package/src/cli.mjs +25 -2
- package/src/direct.mjs +25 -1
- package/src/engine.mjs +84 -6
- package/src/hub.mjs +1 -1
- package/src/mac.mjs +73 -7
- package/src/mcp.mjs +1 -1
- package/src/prompt.mjs +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ 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.9
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Desktop control survives a Peekaboo update: when the Peekaboo Bridge host loses its macOS
|
|
11
|
+
permissions (Screen Recording / Accessibility), the bridge falls back to the local Peekaboo
|
|
12
|
+
runtime and keeps working. If nothing can capture, the tool now says so instead of quietly
|
|
13
|
+
returning an empty element list.
|
|
14
|
+
|
|
15
|
+
## 0.6.8
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Detailed work log: every call is recorded with its arguments, outcome and duration, and the
|
|
19
|
+
evidence is kept next to the log — screenshots taken on the desktop, the accessibility trees
|
|
20
|
+
that were read, menu trees and the list of Spaces.
|
|
21
|
+
- `audit_log` / `audit_artifact` tools and `GET /audit`, `GET /audit/artifact` endpoints let the
|
|
22
|
+
agent look back at what it saw.
|
|
23
|
+
- `arena-bridge audit` lists the calls, `arena-bridge audit artifacts` lists the saved files,
|
|
24
|
+
`arena-bridge audit <id>` prints one entry, `arena-bridge audit prune` removes old artifacts.
|
|
25
|
+
|
|
7
26
|
## 0.6.7
|
|
8
27
|
|
|
9
28
|
### Added
|
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-124%20passed-brightgreen.svg?style=flat-square" alt="tests">
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
@@ -95,6 +95,7 @@ arena-bridge setup --root ~/projects
|
|
|
95
95
|
| Группа | Инструменты | Что это |
|
|
96
96
|
| --- | --- | --- |
|
|
97
97
|
| Файлы | `list_dir` `read_file` `write_file` `edit_file` `delete_path` `search_files` `file_get` `file_put` | работа с файлами в разрешённых папках |
|
|
98
|
+
| Журнал | `audit_log` `audit_artifact` | что делалось, какие снимки и деревья Accessibility сохранены |
|
|
98
99
|
| Шелл | `bash` `sysinfo` | команды и сведения о машине |
|
|
99
100
|
| Процессы | `start_process` `read_process_output` `write_process` `kill_process` | фоновые дев-серверы |
|
|
100
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 |
|
|
@@ -199,6 +200,25 @@ macOS отдаёт приложениям два разных слоя: карт
|
|
|
199
200
|
|
|
200
201
|
---
|
|
201
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
|
+
|
|
202
222
|
## Доверенное окно: работать без подтверждений
|
|
203
223
|
|
|
204
224
|
```bash
|
|
@@ -309,6 +329,8 @@ GET /health liveness (без авторизации)
|
|
|
309
329
|
GET /tools каталог инструментов с JSON-схемами
|
|
310
330
|
GET /prompt этот промпт для агента
|
|
311
331
|
GET /approvals ожидающие подтверждения
|
|
332
|
+
GET /audit журнал работы с артефактами
|
|
333
|
+
GET /audit/artifact сохранённый артефакт (entry=, file=)
|
|
312
334
|
POST /call {"tool":"bash","args":{...}}
|
|
313
335
|
POST /exec {"command":"...","cwd":"...","timeout_sec":120}
|
|
314
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.9",
|
|
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
|
);
|
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.9',
|
|
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
|
@@ -193,6 +193,7 @@ export function macTools(cfg = {}) {
|
|
|
193
193
|
['see', '--tree', '--no-screenshot', '--json', ...targetFlags(args)],
|
|
194
194
|
['see', '--tree', '--no-screenshot', '--no-remote', '--json', ...targetFlags(args)],
|
|
195
195
|
['see', '--tree', '--json', ...targetFlags(args)],
|
|
196
|
+
['see', '--no-remote', '--tree', '--json', ...targetFlags(args)],
|
|
196
197
|
['see', '--json', ...targetFlags(args)],
|
|
197
198
|
];
|
|
198
199
|
}
|
|
@@ -213,7 +214,8 @@ export function macTools(cfg = {}) {
|
|
|
213
214
|
if (!best) best = cand;
|
|
214
215
|
if (res.exit_code === 0 && cand.elements.length) return cand;
|
|
215
216
|
if (res.exit_code === 0 && best.res.exit_code !== 0) best = cand;
|
|
216
|
-
|
|
217
|
+
const denied = isPermissionError(json, res);
|
|
218
|
+
if (res.exit_code !== 0 && !denied && !UNKNOWN.test(res.stderr || '')) break;
|
|
217
219
|
}
|
|
218
220
|
return best;
|
|
219
221
|
};
|
|
@@ -259,6 +261,15 @@ export function macTools(cfg = {}) {
|
|
|
259
261
|
if (!outPath.ok) return { ok: false, error: outPath.error, reason: outPath.reason };
|
|
260
262
|
ensureDir(path.dirname(outPath.abs));
|
|
261
263
|
const seen = await collectSee(args, outPath.abs);
|
|
264
|
+
const failure = peekError(seen.json);
|
|
265
|
+
if (failure && !seen.elements.length) {
|
|
266
|
+
return {
|
|
267
|
+
ok: false,
|
|
268
|
+
error: 'peekaboo_failed',
|
|
269
|
+
reason: failure.message || String(failure.code || 'peekaboo returned an error'),
|
|
270
|
+
hint: PERMISSION_HINT,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
262
273
|
const elements = seen.elements.slice(0, 300);
|
|
263
274
|
const saved = fs.existsSync(outPath.abs);
|
|
264
275
|
if (!saved) {
|
|
@@ -284,6 +295,21 @@ export function macTools(cfg = {}) {
|
|
|
284
295
|
out.hint += ' No elements found — run mac_permissions: the element list comes from the macOS Accessibility API.';
|
|
285
296
|
}
|
|
286
297
|
if (args.include_raw && seen.json) out.raw = seen.json;
|
|
298
|
+
if (ctx.artifact) {
|
|
299
|
+
try {
|
|
300
|
+
if (fs.existsSync(outPath.abs)) {
|
|
301
|
+
ctx.artifact('screenshot', 'screen.png', fs.readFileSync(outPath.abs), { source: 'mac_see' });
|
|
302
|
+
}
|
|
303
|
+
if (seen.json) {
|
|
304
|
+
ctx.artifact('ax-tree', 'ax-tree.json', JSON.stringify(seen.json, null, 2), {
|
|
305
|
+
source: 'mac_see',
|
|
306
|
+
elements: seen.elements.length,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
/* artifacts are best effort */
|
|
311
|
+
}
|
|
312
|
+
}
|
|
287
313
|
return out;
|
|
288
314
|
},
|
|
289
315
|
},
|
|
@@ -593,11 +619,18 @@ export function macTools(cfg = {}) {
|
|
|
593
619
|
[],
|
|
594
620
|
),
|
|
595
621
|
required: [],
|
|
596
|
-
handler: async (args) => {
|
|
622
|
+
handler: async (args, ctx) => {
|
|
597
623
|
const p = checkPeek(peek());
|
|
598
624
|
if (!p.ok) return p;
|
|
599
625
|
const seen = await searchSee(args);
|
|
600
626
|
const matches = seen.elements.filter((e) => matchElement(e, args)).slice(0, Number(args.limit) || 25);
|
|
627
|
+
if (ctx.artifact && seen.json) {
|
|
628
|
+
ctx.artifact('ax-tree', 'ax-tree.json', JSON.stringify(seen.json, null, 2), {
|
|
629
|
+
source: 'mac_element',
|
|
630
|
+
elements: seen.elements.length,
|
|
631
|
+
query: args.name || args.role || null,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
601
634
|
return {
|
|
602
635
|
ok: true,
|
|
603
636
|
matches,
|
|
@@ -645,7 +678,7 @@ export function macTools(cfg = {}) {
|
|
|
645
678
|
approval: false,
|
|
646
679
|
schema: S({ app: { type: 'string' } }, []),
|
|
647
680
|
required: [],
|
|
648
|
-
handler: async (args) => {
|
|
681
|
+
handler: async (args, ctx) => {
|
|
649
682
|
const p = checkPeek(peek());
|
|
650
683
|
if (!p.ok) return p;
|
|
651
684
|
const flags = menuFlags(args);
|
|
@@ -659,6 +692,9 @@ export function macTools(cfg = {}) {
|
|
|
659
692
|
{ timeout: 30 },
|
|
660
693
|
);
|
|
661
694
|
const json = parseJson(res.stdout);
|
|
695
|
+
if (ctx.artifact && json) {
|
|
696
|
+
ctx.artifact('menu-tree', 'menu-tree.json', JSON.stringify(json, null, 2), { source: 'mac_menu' });
|
|
697
|
+
}
|
|
662
698
|
return {
|
|
663
699
|
ok: res.exit_code === 0,
|
|
664
700
|
menus: json ? (json.menus || json.data?.menus || json.data?.menu_structure || json) : undefined,
|
|
@@ -716,7 +752,7 @@ export function macTools(cfg = {}) {
|
|
|
716
752
|
approval: false,
|
|
717
753
|
schema: S({}, []),
|
|
718
754
|
required: [],
|
|
719
|
-
handler: async () => {
|
|
755
|
+
handler: async (args, ctx) => {
|
|
720
756
|
const p = checkPeek(peek());
|
|
721
757
|
if (!p.ok) return p;
|
|
722
758
|
const res = await runVariants(
|
|
@@ -729,6 +765,9 @@ export function macTools(cfg = {}) {
|
|
|
729
765
|
{ timeout: 30 },
|
|
730
766
|
);
|
|
731
767
|
const json = parseJson(res.stdout);
|
|
768
|
+
if (ctx.artifact && json) {
|
|
769
|
+
ctx.artifact('spaces', 'spaces.json', JSON.stringify(json, null, 2), { source: 'mac_spaces' });
|
|
770
|
+
}
|
|
732
771
|
return {
|
|
733
772
|
ok: res.exit_code === 0,
|
|
734
773
|
spaces: json ? (json.spaces || json.data?.spaces || json) : undefined,
|
|
@@ -744,7 +783,7 @@ export function macTools(cfg = {}) {
|
|
|
744
783
|
approval: true,
|
|
745
784
|
schema: S({ to: { type: 'string', description: 'space id or index from mac_spaces' } }, ['to']),
|
|
746
785
|
required: ['to'],
|
|
747
|
-
handler: async (args) => {
|
|
786
|
+
handler: async (args, ctx) => {
|
|
748
787
|
const p = checkPeek(peek());
|
|
749
788
|
if (!p.ok) return p;
|
|
750
789
|
const res = await runVariants(
|
|
@@ -775,14 +814,16 @@ export function macTools(cfg = {}) {
|
|
|
775
814
|
[],
|
|
776
815
|
),
|
|
777
816
|
required: [],
|
|
778
|
-
handler: async (args) => {
|
|
817
|
+
handler: async (args, ctx) => {
|
|
779
818
|
const p = checkPeek(peek());
|
|
780
819
|
if (!p.ok) return p;
|
|
781
820
|
const limit = Math.min(120, Math.max(1, Number(args.timeout) || 20));
|
|
782
821
|
const deadline = Date.now() + limit * 1000;
|
|
783
822
|
let scanned = 0;
|
|
823
|
+
let last = null;
|
|
784
824
|
while (Date.now() < deadline) {
|
|
785
825
|
const seen = await searchSee(args);
|
|
826
|
+
last = seen;
|
|
786
827
|
scanned = seen.elements.length;
|
|
787
828
|
const hit = seen.elements.find((e) => matchElement(e, args));
|
|
788
829
|
if (args.disappear) {
|
|
@@ -792,6 +833,12 @@ export function macTools(cfg = {}) {
|
|
|
792
833
|
}
|
|
793
834
|
await new Promise((r) => setTimeout(r, 2000));
|
|
794
835
|
}
|
|
836
|
+
if (ctx.artifact && last?.json) {
|
|
837
|
+
ctx.artifact('ax-tree', 'ax-tree.json', JSON.stringify(last.json, null, 2), {
|
|
838
|
+
source: 'mac_wait',
|
|
839
|
+
elements: scanned,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
795
842
|
return {
|
|
796
843
|
ok: false,
|
|
797
844
|
found: false,
|
|
@@ -808,7 +855,7 @@ export function macTools(cfg = {}) {
|
|
|
808
855
|
approval: false,
|
|
809
856
|
schema: S({}, []),
|
|
810
857
|
required: [],
|
|
811
|
-
handler: async () => {
|
|
858
|
+
handler: async (args, ctx) => {
|
|
812
859
|
const p = checkPeek(peek());
|
|
813
860
|
if (!p.ok) {
|
|
814
861
|
return {
|
|
@@ -831,6 +878,25 @@ export function macTools(cfg = {}) {
|
|
|
831
878
|
];
|
|
832
879
|
}
|
|
833
880
|
|
|
881
|
+
/** The Peekaboo envelope reports a host that lost its macOS permissions. */
|
|
882
|
+
function peekError(json) {
|
|
883
|
+
if (!json || typeof json !== 'object') return null;
|
|
884
|
+
const err = json.error;
|
|
885
|
+
if (err && typeof err === 'object' && (err.code || err.message)) return err;
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function isPermissionError(json, res) {
|
|
890
|
+
const err = peekError(json);
|
|
891
|
+
const text = `${err?.code || ''} ${err?.message || ''} ${res?.stderr || ''}`;
|
|
892
|
+
return /permission|screen recording|accessibility|event synthesizing/i.test(text);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const PERMISSION_HINT =
|
|
896
|
+
'The Peekaboo host that captures the screen lost its macOS permissions (this happens after a Peekaboo update). ' +
|
|
897
|
+
'Re-grant Screen Recording and Accessibility to "Peekaboo Bridge" in System Settings → Privacy & Security, ' +
|
|
898
|
+
'or let the bridge use the local runtime (--no-remote). Run mac_permissions for the current state.';
|
|
899
|
+
|
|
834
900
|
function menuFlags(args) {
|
|
835
901
|
const f = [];
|
|
836
902
|
if (args.app) f.push('--app', String(args.app));
|
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.9' },
|
|
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
|