@remcp/runtime 0.2.102 → 0.2.104
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 +14 -0
- package/README.md +5 -5
- package/package.json +1 -1
- package/src/config.mjs +3 -4
- package/src/extended/desktop-linux.mjs +91 -5
- package/src/extended/desktop-macos.mjs +145 -10
- package/src/extended/desktop.mjs +22 -8
- package/src/policy.mjs +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.104 — 2026-09-22
|
|
4
|
+
|
|
5
|
+
- Fresh installations default the narrow catastrophic-command guardrail to block; ordinary development, file, Git, package-manager, Docker, database and browser workflows remain available, and explicit warn, allow, or unrestricted/godmode remain operator-controlled opt-ins.
|
|
6
|
+
- macOS top-level window handles include a snapshot generation and retain short-lived app/title/bounds identity so window reordering cannot silently retarget a previous id.
|
|
7
|
+
- A valid prior macOS handle resolves the same uniquely identifiable window after reorder; unknown, ambiguous, expired, PID-mismatched and legacy ids fail closed and request a fresh list_windows.
|
|
8
|
+
- Added 73 Desktop Commander prompt scenarios as an isolated compatibility smoke matrix; all 73 passed on the release tree with no guardrail false positives. GNOME Wayland live computer-use passed 80/80 checks.
|
|
9
|
+
|
|
10
|
+
## 0.2.103 — 2026-09-22
|
|
11
|
+
|
|
12
|
+
- Native Wayland `window_action` targets exact PID/title Accessibility windows for `close` and verifies that the requested window disappeared.
|
|
13
|
+
- Shortcut-backed Wayland window actions require verified focus on the exact requested window before minimize, maximize, move, resize, or close proceeds; ambiguous targeting fails closed.
|
|
14
|
+
- Live smoke coverage isolates XDG open/reveal handlers and stabilizes browser input focus without changing the runtime tool surface.
|
|
15
|
+
- Verified on GNOME Wayland with 80 passing computer-use checks and a two-window isolation proof: target A closed while sibling B stayed open.
|
|
16
|
+
|
|
3
17
|
## 0.2.102 — 2026-09-22
|
|
4
18
|
|
|
5
19
|
- `runWithInput` installs a stdin error handler before writing input, so an early child pipe close is returned as a `ToolError` instead of an uncaught `EPIPE` that can terminate the runtime.
|
package/README.md
CHANGED
|
@@ -147,10 +147,10 @@ in `~/.config/remcp/runtime.json`. `--describe` always reports the current state
|
|
|
147
147
|
against the resolved real path of the deepest existing ancestor rather than the lexical string, so
|
|
148
148
|
`<allowed>/link -> /etc` cannot be used to read or write outside the allowed directories.
|
|
149
149
|
`allowedRoots: ["/"]` means the whole filesystem and works as written.
|
|
150
|
-
- **Command guardrail.** `dangerousCommands` defaults to `
|
|
151
|
-
|
|
152
|
-
*command word* of each shell segment, so
|
|
153
|
-
`blockedCommands` entries are always enforced.
|
|
150
|
+
- **Command guardrail.** `dangerousCommands` defaults to `block`: a catastrophic command is refused
|
|
151
|
+
before execution. Operators can explicitly choose `warn` to run it with an advisory note or `allow`
|
|
152
|
+
to disable the built-in guardrail. Rules match the *command word* of each shell segment, so
|
|
153
|
+
`grep -n format README.md` is never affected. User `blockedCommands` entries are always enforced.
|
|
154
154
|
- **A bad configuration is loud.** If `runtime.json` cannot be parsed, the device refuses to start and
|
|
155
155
|
says why, instead of quietly dropping your `allowedRoots` and re-enabling usage metrics.
|
|
156
156
|
- **Secret masking.** `list_processes` masks command arguments that look like tokens, passwords, or
|
|
@@ -182,7 +182,7 @@ Optional settings live in `~/.config/remcp/runtime.json` (override the directory
|
|
|
182
182
|
"name": "workstation",
|
|
183
183
|
"allowedRoots": ["~/projects", "/srv/data"],
|
|
184
184
|
"blockedCommands": ["rm -rf /", "shutdown"],
|
|
185
|
-
"dangerousCommands": "
|
|
185
|
+
"dangerousCommands": "block",
|
|
186
186
|
"telemetryEnabled": true,
|
|
187
187
|
"maxOutputBytes": 1048576,
|
|
188
188
|
"maxReadLines": 2000,
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -61,10 +61,9 @@ function booleanValue(value, fallback) {
|
|
|
61
61
|
|
|
62
62
|
const DANGEROUS_MODES = ['block', 'warn', 'allow'];
|
|
63
63
|
|
|
64
|
-
// `
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
function dangerousMode(value, fallback = 'warn') {
|
|
64
|
+
// `block` is the secure default: matching catastrophic commands are refused before execution.
|
|
65
|
+
// Operators can explicitly choose `warn` to run with an advisory note or `allow` for zero guardrail noise.
|
|
66
|
+
function dangerousMode(value, fallback = 'block') {
|
|
68
67
|
const normalized = String(value ?? '').trim().toLowerCase();
|
|
69
68
|
return DANGEROUS_MODES.includes(normalized) ? normalized : fallback;
|
|
70
69
|
}
|
|
@@ -312,19 +312,100 @@ async function verifyNativeWaylandGeometry(row, expected, label) {
|
|
|
312
312
|
throw new Error(`Could not verify native Wayland window ${label} for ${row.title || row.app || row.id}: wanted ${wanted}; observed ${actual}`);
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
async function nativeWaylandAccessibilityAction(row, actionNames) {
|
|
316
|
+
const pid = Number(row?.pid);
|
|
317
|
+
if (!Number.isInteger(pid) || pid <= 0 || !Array.isArray(actionNames) || !actionNames.length) {
|
|
318
|
+
return { applied:false, available:[] };
|
|
319
|
+
}
|
|
320
|
+
const script = pyAtSpiPrelude() + `
|
|
321
|
+
want_pid=${pid};want_window=${JSON.stringify(String(row.title || ''))};wanted_actions=${JSON.stringify(actionNames)}
|
|
322
|
+
target=None
|
|
323
|
+
for root,app_name,pid,active in q:
|
|
324
|
+
if pid!=want_pid: continue
|
|
325
|
+
try:
|
|
326
|
+
for child in root:
|
|
327
|
+
try:
|
|
328
|
+
role=(child.getRoleName() or "").lower();name=child.name or ""
|
|
329
|
+
if role not in ("frame","window","dialog"): continue
|
|
330
|
+
if want_window and want_window.strip().lower()!=name.strip().lower(): continue
|
|
331
|
+
target=child;break
|
|
332
|
+
except: pass
|
|
333
|
+
except: pass
|
|
334
|
+
if target is not None: break
|
|
335
|
+
if target is None:
|
|
336
|
+
print(json.dumps({"applied":False,"available":[],"reason":"target window not found"}))
|
|
337
|
+
else:
|
|
338
|
+
try:
|
|
339
|
+
actions=target.queryAction();available=[];chosen=-1;chosen_name=""
|
|
340
|
+
for i in range(actions.nActions):
|
|
341
|
+
try: action_name=actions.getName(i) or ""
|
|
342
|
+
except: action_name=""
|
|
343
|
+
available.append(action_name)
|
|
344
|
+
if chosen<0 and action_name in wanted_actions:
|
|
345
|
+
chosen=i;chosen_name=action_name
|
|
346
|
+
if chosen<0:
|
|
347
|
+
print(json.dumps({"applied":False,"available":available,"reason":"requested accessibility action unavailable"}))
|
|
348
|
+
else:
|
|
349
|
+
applied=actions.doAction(chosen)
|
|
350
|
+
if applied is False: raise Exception("accessibility window action returned false")
|
|
351
|
+
print(json.dumps({"applied":True,"action":chosen_name,"available":available}))
|
|
352
|
+
except Exception as exc:
|
|
353
|
+
print(json.dumps({"applied":False,"available":[],"reason":str(exc)}))
|
|
354
|
+
`;
|
|
355
|
+
const result = await runFile('python3', ['-c', script], { label:'AT-SPI window action', timeout:2000, allowFailure:true });
|
|
356
|
+
try {
|
|
357
|
+
const payload = JSON.parse(result.stdout || '{}');
|
|
358
|
+
return payload && typeof payload === 'object' ? payload : { applied:false, available:[] };
|
|
359
|
+
} catch {
|
|
360
|
+
return { applied:false, available:[], reason:String(result.stderr || result.stdout || 'AT-SPI window action failed').trim() };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function verifyNativeWaylandWindowClosed(row, timeoutMs = 2500) {
|
|
365
|
+
const deadline = Date.now() + timeoutMs;
|
|
366
|
+
while (Date.now() < deadline) {
|
|
367
|
+
try { await windowMatch({ id:row.id }); }
|
|
368
|
+
catch { return; }
|
|
369
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
370
|
+
}
|
|
371
|
+
throw new Error(`Could not verify native Wayland window close for ${row.title || row.app || row.id}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function focusNativeWaylandForAction(row, backend) {
|
|
375
|
+
await windowAction({ action:'focus', id:row.id, backend });
|
|
376
|
+
const focusState = await targetAccessibilityFocus(row);
|
|
377
|
+
if (!focusState.exact) throw new Error(`Could not verify native Wayland focus for ${row.title || row.app || row.id}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
315
380
|
async function nativeWaylandWindowAction(row, action, args) {
|
|
316
381
|
const backend = inputBackend(args);
|
|
317
382
|
if (backend === 'x11') unavailable('Native Wayland window action', 'use backend=portal for compositor-managed windows');
|
|
318
383
|
if (!waylandPortalCandidate()) unavailable('Native Wayland window action', 'no XDG RemoteDesktop portal is available');
|
|
319
384
|
if (backend === 'auto' && !hasWaylandRemoteDesktopGrant()) portalPermissionHint('Native Wayland window action');
|
|
320
|
-
if (row.ui_id) {
|
|
321
|
-
try { await uiAction({ action:'focus', id:row.ui_id }); } catch {}
|
|
322
|
-
}
|
|
323
385
|
|
|
324
386
|
const portalBackend = backend === 'portal' ? 'portal' : 'auto';
|
|
325
387
|
const timeoutMs = backend === 'portal' ? 120_000 : 2500;
|
|
326
|
-
if (action === 'close')
|
|
327
|
-
|
|
388
|
+
if (action === 'close') {
|
|
389
|
+
const semantic = await nativeWaylandAccessibilityAction(row, ['window.close']);
|
|
390
|
+
if (semantic.applied) {
|
|
391
|
+
await verifyNativeWaylandWindowClosed(row);
|
|
392
|
+
return jsonResult({ action, backend:'atspi-window-action', id:row.id, accessibility_action:semantic.action });
|
|
393
|
+
}
|
|
394
|
+
// Some GTK apps disconnect from AT-SPI while handling window.close, before the action
|
|
395
|
+
// call can return a success payload. Trust the observed target state, not that race.
|
|
396
|
+
try {
|
|
397
|
+
await verifyNativeWaylandWindowClosed(row, 400);
|
|
398
|
+
return jsonResult({ action, backend:'atspi-window-action', id:row.id, accessibility_action:'window.close', verified_after_disconnect:true });
|
|
399
|
+
} catch {}
|
|
400
|
+
await focusNativeWaylandForAction(row, portalBackend);
|
|
401
|
+
await portalShortcut('ALT+F4', { timeoutMs });
|
|
402
|
+
await verifyNativeWaylandWindowClosed(row);
|
|
403
|
+
return jsonResult({ action, backend:'xdg-desktop-portal', id:row.id });
|
|
404
|
+
}
|
|
405
|
+
if (action === 'minimize') {
|
|
406
|
+
await focusNativeWaylandForAction(row, portalBackend);
|
|
407
|
+
await portalShortcut('ALT+F9', { timeoutMs });
|
|
408
|
+
}
|
|
328
409
|
else if (action === 'maximize' || action === 'restore') {
|
|
329
410
|
let maximized = false;
|
|
330
411
|
try {
|
|
@@ -346,9 +427,11 @@ async function nativeWaylandWindowAction(row, action, args) {
|
|
|
346
427
|
}
|
|
347
428
|
} catch {}
|
|
348
429
|
if ((action === 'maximize' && !maximized) || (action === 'restore' && maximized)) {
|
|
430
|
+
await focusNativeWaylandForAction(row, portalBackend);
|
|
349
431
|
await portalShortcut('ALT+F10', { timeoutMs });
|
|
350
432
|
}
|
|
351
433
|
} else {
|
|
434
|
+
await focusNativeWaylandForAction(row, portalBackend);
|
|
352
435
|
let currentX = Number(row.x), currentY = Number(row.y), currentWidth = Number(row.width), currentHeight = Number(row.height);
|
|
353
436
|
if (![currentX,currentY,currentWidth,currentHeight].every(Number.isFinite)) unavailable('Native Wayland window geometry', 'accessibility bounds are unavailable');
|
|
354
437
|
|
|
@@ -495,6 +578,9 @@ except Exception:
|
|
|
495
578
|
def currentValue(self,v): self.a.set_current_value(float(v))
|
|
496
579
|
class _Action:
|
|
497
580
|
def __init__(self,a): self.a=a
|
|
581
|
+
@property
|
|
582
|
+
def nActions(self): return self.a.get_n_actions()
|
|
583
|
+
def getName(self,i): return self.a.get_action_name(i)
|
|
498
584
|
def doAction(self,i): return self.a.do_action(i)
|
|
499
585
|
class _Editable:
|
|
500
586
|
def __init__(self,a): self.a=a
|
|
@@ -23,6 +23,72 @@ function parseTsv(value, fields) {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
const macWindowTargets = new Map();
|
|
27
|
+
const MAC_WINDOW_TARGET_TTL_MS = 5 * 60 * 1000;
|
|
28
|
+
let macWindowGeneration = 0;
|
|
29
|
+
|
|
30
|
+
function rememberMacWindowTarget(row) {
|
|
31
|
+
macWindowTargets.set(row.id, {
|
|
32
|
+
pid:row.pid,
|
|
33
|
+
app:row.app,
|
|
34
|
+
title:row.title,
|
|
35
|
+
x:row.x,
|
|
36
|
+
y:row.y,
|
|
37
|
+
width:row.width,
|
|
38
|
+
height:row.height,
|
|
39
|
+
at:Date.now(),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cachedMacWindowTarget(id) {
|
|
44
|
+
const cached = macWindowTargets.get(id);
|
|
45
|
+
if (!cached) return null;
|
|
46
|
+
if ((Date.now() - cached.at) > MAC_WINDOW_TARGET_TTL_MS) {
|
|
47
|
+
macWindowTargets.delete(id);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return cached;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function macWindowId(pid, windowIndex, generation) {
|
|
54
|
+
const numericPid = Number(pid);
|
|
55
|
+
const numericIndex = Number(windowIndex);
|
|
56
|
+
const numericGeneration = Number(generation);
|
|
57
|
+
if (!Number.isInteger(numericPid) || numericPid <= 0
|
|
58
|
+
|| !Number.isInteger(numericIndex) || numericIndex < 0
|
|
59
|
+
|| !Number.isInteger(numericGeneration) || numericGeneration <= 0) {
|
|
60
|
+
throw new Error('Could not construct a macOS window id from invalid PID/index/generation');
|
|
61
|
+
}
|
|
62
|
+
return `mac:${numericPid}:w${numericIndex}:g${numericGeneration}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function normalizeMacWindowTarget(args = {}, { useWindowId = false } = {}) {
|
|
66
|
+
const explicitPid = Number.isInteger(Number(args.pid)) && Number(args.pid) > 0 ? Number(args.pid) : 0;
|
|
67
|
+
let idPid = 0;
|
|
68
|
+
let windowIndex = null;
|
|
69
|
+
const rawId = useWindowId ? optionalString(args.id) : '';
|
|
70
|
+
let cached = null;
|
|
71
|
+
if (rawId) {
|
|
72
|
+
const match = /^mac:(\d+):w(\d+):g(\d+)$/.exec(rawId);
|
|
73
|
+
if (!match) throw new Error('macOS window id is stale or unsupported; call list_windows again');
|
|
74
|
+
idPid = Number(match[1]);
|
|
75
|
+
windowIndex = Number(match[2]);
|
|
76
|
+
if (explicitPid && explicitPid !== idPid) throw new Error('macOS window id and pid refer to different processes');
|
|
77
|
+
cached = cachedMacWindowTarget(rawId);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
id:rawId,
|
|
81
|
+
idCached:Boolean(cached),
|
|
82
|
+
pid:idPid || explicitPid,
|
|
83
|
+
windowIndex,
|
|
84
|
+
app:optionalString(args.app) || '',
|
|
85
|
+
title:optionalString(args.window_title || args.windowTitle || args.title) || '',
|
|
86
|
+
expectedApp:cached?.app || '',
|
|
87
|
+
expectedTitle:cached?.title || '',
|
|
88
|
+
expectedBounds:cached ? [cached.x,cached.y,cached.width,cached.height] : null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
26
92
|
export async function listWindows() {
|
|
27
93
|
const script = `
|
|
28
94
|
set output to ""
|
|
@@ -31,22 +97,30 @@ tell application "System Events"
|
|
|
31
97
|
try
|
|
32
98
|
set appName to name of p
|
|
33
99
|
set appPid to unix id of p
|
|
100
|
+
set windowIndex to 0
|
|
34
101
|
repeat with w in windows of p
|
|
35
102
|
try
|
|
36
103
|
set pos to position of w
|
|
37
104
|
set sz to size of w
|
|
38
|
-
set output to output & appPid & tab & appName & tab & (name of w) & tab & (item 1 of pos) & tab & (item 2 of pos) & tab & (item 1 of sz) & tab & (item 2 of sz) & linefeed
|
|
105
|
+
set output to output & appPid & tab & windowIndex & tab & appName & tab & (name of w) & tab & (item 1 of pos) & tab & (item 2 of pos) & tab & (item 1 of sz) & tab & (item 2 of sz) & linefeed
|
|
39
106
|
end try
|
|
107
|
+
set windowIndex to windowIndex + 1
|
|
40
108
|
end repeat
|
|
41
109
|
end try
|
|
42
110
|
end repeat
|
|
43
111
|
end tell
|
|
44
112
|
return output`;
|
|
45
113
|
const { stdout } = await runOsa(script, { label: 'list windows' });
|
|
46
|
-
|
|
47
|
-
|
|
114
|
+
macWindowGeneration += 1;
|
|
115
|
+
const generation = macWindowGeneration;
|
|
116
|
+
const rows = parseTsv(stdout, ['pid','window_index','app','title','x','y','width','height']).map(row=>({
|
|
117
|
+
id:macWindowId(row.pid,row.window_index,generation),pid:Number(row.pid),app:row.app,title:row.title,
|
|
48
118
|
x:Number(row.x),y:Number(row.y),width:Number(row.width),height:Number(row.height),
|
|
49
119
|
}));
|
|
120
|
+
for (const [id,cached] of macWindowTargets.entries()) {
|
|
121
|
+
if ((Date.now() - cached.at) > MAC_WINDOW_TARGET_TTL_MS) macWindowTargets.delete(id);
|
|
122
|
+
}
|
|
123
|
+
for (const row of rows) rememberMacWindowTarget(row);
|
|
50
124
|
return jsonResult(rows);
|
|
51
125
|
}
|
|
52
126
|
|
|
@@ -67,7 +141,7 @@ export async function windowAction(args = {}) {
|
|
|
67
141
|
if((action==='move'||action==='move_resize')&&(!Number.isFinite(x)||!Number.isFinite(y)))throw new Error('x and y are required for window move');
|
|
68
142
|
if((action==='resize'||action==='move_resize')&&(!Number.isFinite(width)||!Number.isFinite(height)||width<=0||height<=0))throw new Error('width and height must be positive for window resize');
|
|
69
143
|
const script=`(function(){
|
|
70
|
-
${macJxaTargetPrelude(args)}
|
|
144
|
+
${macJxaTargetPrelude(args,{useWindowId:true})}
|
|
71
145
|
function perform(e,wanted){let acts=[];try{acts=e.actions()}catch(_){};for(let i=0;i<acts.length;i++){let n='';try{n=String(acts[i].name())}catch(_){};if(n===wanted){acts[i].perform();return true}}return false}
|
|
72
146
|
function setAttr(e,n,v){try{e.attributes.byName(n).value=v;return true}catch(_){return false}}
|
|
73
147
|
const action=${JSON.stringify(action)};
|
|
@@ -86,21 +160,58 @@ return JSON.stringify({action,pid:Number(proc.unixId()),app:safeName(proc),windo
|
|
|
86
160
|
let parsed;try{parsed=JSON.parse(stdout.trim())}catch{parsed={action,backend:'macos-accessibility'}}
|
|
87
161
|
return jsonResult(parsed);
|
|
88
162
|
}
|
|
89
|
-
function
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
163
|
+
export async function resolveWindowTarget(args = {}) {
|
|
164
|
+
const script=`(function(){
|
|
165
|
+
${macJxaTargetPrelude(args,{useWindowId:true})}
|
|
166
|
+
let p={};try{p=win.properties()}catch(_){};
|
|
167
|
+
return JSON.stringify({
|
|
168
|
+
id:${JSON.stringify(optionalString(args.id || args.window_id || args.windowId) || '')},
|
|
169
|
+
pid:Number(proc.unixId()),app:safeName(proc),title:safeName(win),
|
|
170
|
+
x:Array.isArray(p.position)?Number(p.position[0]):null,
|
|
171
|
+
y:Array.isArray(p.position)?Number(p.position[1]):null,
|
|
172
|
+
width:Array.isArray(p.size)?Number(p.size[0]):null,
|
|
173
|
+
height:Array.isArray(p.size)?Number(p.size[1]):null,
|
|
174
|
+
backend:'macos-accessibility'
|
|
175
|
+
});
|
|
176
|
+
})()`;
|
|
177
|
+
const {stdout}=await runOsa(script,{javascript:true,label:'resolve window target',timeout:30_000});
|
|
178
|
+
let parsed;try{parsed=JSON.parse(stdout.trim())}catch{throw new Error('Could not resolve macOS window target')}
|
|
179
|
+
return jsonResult(parsed);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function macJxaTargetPrelude(args = {}, options = {}) {
|
|
183
|
+
const target = normalizeMacWindowTarget(args, options);
|
|
184
|
+
if (options.useWindowId && target.id && !target.idCached) throw new Error('macOS window id is stale or unknown; call list_windows again');
|
|
185
|
+
const app = JSON.stringify(target.app);
|
|
186
|
+
const title = JSON.stringify(target.title);
|
|
187
|
+
const expectedApp = JSON.stringify(target.expectedApp);
|
|
188
|
+
const expectedTitle = JSON.stringify(target.expectedTitle);
|
|
189
|
+
const expectedBounds = JSON.stringify(target.expectedBounds);
|
|
93
190
|
return String.raw`
|
|
94
191
|
const se=Application('System Events');
|
|
95
|
-
const wantPid=${pid};
|
|
192
|
+
const wantPid=${target.pid};
|
|
193
|
+
const wantWindowIndex=${target.windowIndex == null ? 'null' : target.windowIndex};
|
|
96
194
|
const wantApp=${app};
|
|
97
195
|
const wantTitle=${title};
|
|
196
|
+
const expectedApp=${expectedApp};
|
|
197
|
+
const expectedTitle=${expectedTitle};
|
|
198
|
+
const expectedBounds=${expectedBounds};
|
|
98
199
|
function safeWindows(p){try{return p.windows()}catch(e){return []}}
|
|
99
200
|
function safeName(spec){try{return String(spec.name()||'')}catch(e){return ''}}
|
|
201
|
+
function safeProps(spec){try{return spec.properties()}catch(e){return {}}}
|
|
202
|
+
function sameBounds(spec,bounds){
|
|
203
|
+
if(!bounds)return false;
|
|
204
|
+
const p=safeProps(spec),pos=Array.isArray(p.position)?p.position:[],size=Array.isArray(p.size)?p.size:[];
|
|
205
|
+
return Number(pos[0])===Number(bounds[0])&&Number(pos[1])===Number(bounds[1])&&Number(size[0])===Number(bounds[2])&&Number(size[1])===Number(bounds[3]);
|
|
206
|
+
}
|
|
100
207
|
function chooseProcess(){
|
|
101
208
|
if(wantPid){
|
|
102
209
|
const rows=se.applicationProcesses.whose({unixId:wantPid})();
|
|
103
|
-
if(rows.length)
|
|
210
|
+
if(rows.length){
|
|
211
|
+
if(wantApp && !safeName(rows[0]).toLowerCase().includes(wantApp.toLowerCase()))throw new Error('macOS window target app does not match the requested id/pid');
|
|
212
|
+
if(expectedApp && safeName(rows[0])!==expectedApp)throw new Error('macOS window id no longer belongs to the same application; call list_windows again');
|
|
213
|
+
return rows[0];
|
|
214
|
+
}
|
|
104
215
|
throw new Error('No matching UI process found');
|
|
105
216
|
}
|
|
106
217
|
if(wantApp){
|
|
@@ -130,6 +241,30 @@ function chooseProcess(){
|
|
|
130
241
|
function chooseWindow(p){
|
|
131
242
|
const rows=safeWindows(p);
|
|
132
243
|
if(!rows.length)throw new Error('The selected application has no accessible windows');
|
|
244
|
+
if(wantWindowIndex!==null){
|
|
245
|
+
const indexValid=Number.isInteger(wantWindowIndex)&&wantWindowIndex>=0&&wantWindowIndex<rows.length;
|
|
246
|
+
const selected=indexValid?rows[wantWindowIndex]:null;
|
|
247
|
+
if(expectedTitle){
|
|
248
|
+
if(selected&&safeName(selected)===expectedTitle)return selected;
|
|
249
|
+
const matches=[];
|
|
250
|
+
for(let i=0;i<rows.length;i++)if(safeName(rows[i])===expectedTitle)matches.push(rows[i]);
|
|
251
|
+
if(matches.length===1)return matches[0];
|
|
252
|
+
if(matches.length>1&&expectedBounds){
|
|
253
|
+
const bounded=matches.filter(row=>sameBounds(row,expectedBounds));
|
|
254
|
+
if(bounded.length===1)return bounded[0];
|
|
255
|
+
}
|
|
256
|
+
throw new Error('macOS window id no longer identifies one unambiguous window; call list_windows again');
|
|
257
|
+
}
|
|
258
|
+
if(expectedBounds){
|
|
259
|
+
if(selected&&sameBounds(selected,expectedBounds))return selected;
|
|
260
|
+
const bounded=rows.filter(row=>sameBounds(row,expectedBounds));
|
|
261
|
+
if(bounded.length===1)return bounded[0];
|
|
262
|
+
throw new Error('macOS window id no longer identifies one unambiguous window; call list_windows again');
|
|
263
|
+
}
|
|
264
|
+
if(!indexValid)throw new Error('macOS window id is stale; call list_windows again');
|
|
265
|
+
if(wantTitle && !safeName(selected).toLowerCase().includes(wantTitle.toLowerCase()))throw new Error('macOS window id no longer matches the requested title; call list_windows again');
|
|
266
|
+
return selected;
|
|
267
|
+
}
|
|
133
268
|
if(wantTitle){
|
|
134
269
|
for(let i=0;i<rows.length;i++){
|
|
135
270
|
if(safeName(rows[i]).toLowerCase().includes(wantTitle.toLowerCase()))return rows[i];
|
package/src/extended/desktop.mjs
CHANGED
|
@@ -355,7 +355,11 @@ export async function uiAction(args = {}) {
|
|
|
355
355
|
if (['set_value','set_range_value'].includes(action) && resolved.value == null) throw new Error(`ui_action ${action} requires value`);
|
|
356
356
|
return adapter.uiAction(resolved);
|
|
357
357
|
}
|
|
358
|
-
export async function keyboard(args = {}) {
|
|
358
|
+
export async function keyboard(args = {}) {
|
|
359
|
+
const explicitWindowTarget = explicitTypeTextWindowTarget(args, false);
|
|
360
|
+
if (explicitWindowTarget) await windowAction({ action:'focus', ...explicitWindowTarget });
|
|
361
|
+
return adapter.keyboard(args);
|
|
362
|
+
}
|
|
359
363
|
export async function pointer(args = {}) {
|
|
360
364
|
if (args.action === 'move' && ![args.x,args.y].every(value => Number.isFinite(Number(value)))) throw new Error('pointer move requires x and y');
|
|
361
365
|
return adapter.pointer(args);
|
|
@@ -373,13 +377,23 @@ export async function screenshotRegion(args = {}) {
|
|
|
373
377
|
const title = optionalString(args.title);
|
|
374
378
|
const pid = Number.isInteger(Number(args.pid)) ? Number(args.pid) : null;
|
|
375
379
|
if (windowId || app || title || pid != null) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
(
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
380
|
+
let row = null;
|
|
381
|
+
if (windowId && typeof adapter.resolveWindowTarget === 'function') {
|
|
382
|
+
row = resultValue(await adapter.resolveWindowTarget({
|
|
383
|
+
id:windowId,
|
|
384
|
+
...(pid != null ? { pid } : {}),
|
|
385
|
+
...(app ? { app } : {}),
|
|
386
|
+
...(title ? { title } : {}),
|
|
387
|
+
}));
|
|
388
|
+
} else {
|
|
389
|
+
const rows = resultValue(await listWindows({ include_monitor:false }));
|
|
390
|
+
row = Array.isArray(rows) ? rows.find(item =>
|
|
391
|
+
(!windowId || item.id === windowId)
|
|
392
|
+
&& (pid == null || Number(item.pid) === pid)
|
|
393
|
+
&& (!app || String(item.app || '').toLowerCase().includes(app.toLowerCase()))
|
|
394
|
+
&& (!title || String(item.title || '').toLowerCase().includes(title.toLowerCase()))
|
|
395
|
+
) : null;
|
|
396
|
+
}
|
|
383
397
|
if (!row) throw new Error('No matching window found for screenshot_region');
|
|
384
398
|
x = Number(row.x); y = Number(row.y); width = Number(row.width); height = Number(row.height);
|
|
385
399
|
} else if (args.monitor != null || args.monitor_index != null) {
|
package/src/policy.mjs
CHANGED
|
@@ -2,10 +2,10 @@ import { runtimeConfig } from './config.mjs';
|
|
|
2
2
|
import { recordEvent } from './telemetry.mjs';
|
|
3
3
|
import { fail } from './util.mjs';
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// Built-in hardening rules for catastrophic host-level commands. The default is `block`:
|
|
6
|
+
// fresh installations refuse these commands before execution. Operators can explicitly choose
|
|
7
|
+
// `warn` (run and report) or `allow` (disable this built-in guardrail); unrestricted/godmode
|
|
8
|
+
// also forces `allow` and remains a local-only opt-in.
|
|
9
9
|
//
|
|
10
10
|
// Rules are matched against the *command word* of each shell segment, so a read-only
|
|
11
11
|
// command that merely mentions a dangerous word (`grep -n format README.md`,
|