@remcp/runtime 0.2.236 → 0.2.237
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 +9 -0
- package/package.json +1 -1
- package/src/compact-catalog.mjs +5 -7
- package/src/extended/catalog.mjs +29 -4
- package/src/extended/desktop-computer-actions.mjs +38 -0
- package/src/extended/desktop-computer-snapshot.mjs +17 -3
- package/src/extended/desktop-webrtc-bridge.mjs +59 -10
- package/src/extended/desktop-windows-session.mjs +89 -0
- package/src/extended/diagnostics-recording.mjs +3 -0
- package/src/index.mjs +5 -3
- package/src/mcp-v2.mjs +8 -8
- package/src/tool-availability.mjs +23 -0
- package/src/tools/files-screenshots.mjs +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.237 — 2026-11-06
|
|
4
|
+
|
|
5
|
+
- Detect Windows Session 0 without enumerating users or modifying Windows login/security settings, and report native desktop_session unavailable when no interactive process desktop exists.
|
|
6
|
+
- Hide native Windows GUI, capture and recording tools from advertised capability lists in Session 0 while retaining terminal, filesystem, document and independently available CDP/browser tools.
|
|
7
|
+
- Fail closed before invoking native desktop actions, screenshots and recording, including mixed action preflights.
|
|
8
|
+
- Cache only capability discovery, never action authorization; emit one atomic tools/list_changed notification when browser availability changes.
|
|
9
|
+
- Gate an experimental Linux WebRTC Stop acknowledgement on confirmed exit of its directly owned Python media worker, with bounded SIGTERM/SIGKILL escalation and fail-closed local admission. Do not equate this with signed process-tree or macOS/Windows native Stop proof; the feature remains off by default.
|
|
10
|
+
- Preserve runtime protocol compatibility and the published 0.2.235/0.2.236 fixes. Locked-user and background service session transitions require further native validation.
|
|
11
|
+
|
|
3
12
|
## 0.2.236 — 2026-11-05
|
|
4
13
|
|
|
5
14
|
- Report CDP readiness only for eligible page targets and classify configured unavailable CDP connections with bounded diagnostics.
|
package/package.json
CHANGED
package/src/compact-catalog.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { TEXT_OUTPUT_SCHEMA,
|
|
2
|
-
import {
|
|
1
|
+
import { TEXT_OUTPUT_SCHEMA, toolDefinitions } from './catalog.mjs';
|
|
2
|
+
import { extendedToolDefinitions } from './extended/catalog.mjs';
|
|
3
|
+
import { supportedRuntimeToolDefinitions } from './tool-availability.mjs';
|
|
3
4
|
|
|
4
5
|
export const compactRuntimeGroups = Object.freeze({
|
|
5
6
|
manage_files: Object.freeze([
|
|
@@ -126,11 +127,8 @@ function groupedDefinition(name, members) {
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
export async function supportedRuntimeOperationNames() {
|
|
129
|
-
const
|
|
130
|
-
return new Set([
|
|
131
|
-
...supportedCoreTools().map(tool => tool.name),
|
|
132
|
-
...extendedToolDefinitions.filter(tool => supportedExtended.has(tool.name)).map(tool => tool.name),
|
|
133
|
-
]);
|
|
130
|
+
const { core, extended } = await supportedRuntimeToolDefinitions();
|
|
131
|
+
return new Set([...core, ...extended].map(tool => tool.name));
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
export async function compactRuntimeToolDefinitions() {
|
package/src/extended/catalog.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { isWaylandSession } from '../screenshot-portal.mjs';
|
|
|
11
11
|
import { waylandPortalCandidate } from '../wayland-remote-desktop.mjs';
|
|
12
12
|
import { commandExists, desktopSessionAvailable, runFile } from './common.mjs';
|
|
13
13
|
import { desktopHandlers } from './desktop.mjs';
|
|
14
|
+
import { cachedWindowsDesktopSessionStatus } from './desktop-windows-session.mjs';
|
|
14
15
|
import { displayInventoryAvailable, regionScreenshotBackends } from './desktop-linux.mjs';
|
|
15
16
|
import { hydrateLinuxDesktopEnv } from './desktop-linux-session-env.mjs';
|
|
16
17
|
import { diagnosticHandlers, gnomeScreencastSupported, recordScreenAvailable } from './diagnostics.mjs';
|
|
@@ -254,6 +255,24 @@ export function documentCapabilitySnapshot({
|
|
|
254
255
|
};
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
// A paired Windows service may be online in Session 0 while native desktop
|
|
259
|
+
// tools are impossible. Keep computer_snapshot and CDP discoverable; they
|
|
260
|
+
// provide their own unavailable status and/or browser-only operation.
|
|
261
|
+
export const WINDOWS_SESSION_SCOPED_CAPABILITIES = Object.freeze([
|
|
262
|
+
'desktop_session', 'desktop_open', 'windows', 'ui', 'input', 'pointer',
|
|
263
|
+
'drag', 'clipboard', 'displays', 'screen_region', 'notifications', 'record',
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
export function limitWindowsSessionCapabilities(capabilities, {
|
|
267
|
+
platform = process.platform,
|
|
268
|
+
sessionStatus = null,
|
|
269
|
+
} = {}) {
|
|
270
|
+
if (platform !== 'win32' || sessionStatus?.available === true) return capabilities;
|
|
271
|
+
const bounded = { ...capabilities };
|
|
272
|
+
for (const name of WINDOWS_SESSION_SCOPED_CAPABILITIES) bounded[name] = false;
|
|
273
|
+
return bounded;
|
|
274
|
+
}
|
|
275
|
+
|
|
257
276
|
export async function capabilitySnapshot() {
|
|
258
277
|
const win = process.platform === 'win32';
|
|
259
278
|
const mac = process.platform === 'darwin';
|
|
@@ -270,7 +289,7 @@ export async function capabilitySnapshot() {
|
|
|
270
289
|
const displayInventoryReady = linux && desktopSession ? await displayInventoryAvailable() : false;
|
|
271
290
|
const browserCdp = browserRemoteEnabled() && await browserControlAvailable(undefined, 500);
|
|
272
291
|
const documentCapabilities = documentCapabilitySnapshot();
|
|
273
|
-
|
|
292
|
+
const capabilities = {
|
|
274
293
|
...desktopShellCapabilities,
|
|
275
294
|
webrtc_pilot:linux && desktopSession && webRtcWorkerAvailable(),
|
|
276
295
|
windows: win || mac || (desktopSession && (commandExists('wdotool') || commandExists('wmctrl') || (wayland && pyAtSpi))),
|
|
@@ -293,6 +312,12 @@ export async function capabilitySnapshot() {
|
|
|
293
312
|
record: recordScreenAvailable({ platform:process.platform, wayland, gnomeSupported:gnomeRecord }),
|
|
294
313
|
...documentCapabilities,
|
|
295
314
|
};
|
|
315
|
+
return win
|
|
316
|
+
? limitWindowsSessionCapabilities(capabilities, {
|
|
317
|
+
platform:process.platform,
|
|
318
|
+
sessionStatus:await cachedWindowsDesktopSessionStatus(),
|
|
319
|
+
})
|
|
320
|
+
: capabilities;
|
|
296
321
|
}
|
|
297
322
|
|
|
298
323
|
function isSupported(definition, capabilities) {
|
|
@@ -308,9 +333,9 @@ export function allExtendedTools() {
|
|
|
308
333
|
return extendedToolDefinitions.map(publicShape);
|
|
309
334
|
}
|
|
310
335
|
|
|
311
|
-
export async function advertisedExtendedTools() {
|
|
312
|
-
const
|
|
313
|
-
return extendedToolDefinitions.filter(definition => isSupported(definition,
|
|
336
|
+
export async function advertisedExtendedTools(capabilities = null) {
|
|
337
|
+
const current = capabilities ?? await capabilitySnapshot();
|
|
338
|
+
return extendedToolDefinitions.filter(definition => isSupported(definition, current)).map(publicShape);
|
|
314
339
|
}
|
|
315
340
|
|
|
316
341
|
export async function capabilityFingerprint() {
|
|
@@ -16,6 +16,7 @@ import { clipboard, displayInventory, keyboard, listWindows, notification, point
|
|
|
16
16
|
import { dragDrop, launchApp, openPath, revealPath, typeText, uiFind, waitForUi } from './desktop-ui-actions.mjs';
|
|
17
17
|
import { clickOcrText } from './desktop-vision.mjs';
|
|
18
18
|
import { computerSnapshot } from './desktop-computer-snapshot.mjs';
|
|
19
|
+
import { assertWindowsInteractiveDesktop, guardWindowsDesktopTool } from './desktop-windows-session.mjs';
|
|
19
20
|
|
|
20
21
|
export function browserComputerAction(args, action) {
|
|
21
22
|
const mapped = {
|
|
@@ -368,3 +369,40 @@ export const desktopHandlers = {
|
|
|
368
369
|
reveal_path: revealPath,
|
|
369
370
|
notification,
|
|
370
371
|
};
|
|
372
|
+
|
|
373
|
+
// Explicit browser-only actions and non-UI waits remain usable with no Windows
|
|
374
|
+
// desktop. Auto-targeted actions may need native input, so they fail closed.
|
|
375
|
+
// Mixed batches must not execute early browser steps before later unsafe native
|
|
376
|
+
// operations are rejected.
|
|
377
|
+
export function computerActionRequiresDesktop(args = {}) {
|
|
378
|
+
const action = String(args?.action || '');
|
|
379
|
+
const target = String(args?.target || args?.target_type || 'auto');
|
|
380
|
+
if (action === 'wait') return false;
|
|
381
|
+
if (action === 'batch') {
|
|
382
|
+
return !Array.isArray(args.steps) || args.steps.some(computerActionRequiresDesktop);
|
|
383
|
+
}
|
|
384
|
+
if (action === 'keyboard') {
|
|
385
|
+
return !(target === 'browser' && args.key && !args.shortcut && !Array.isArray(args.keys));
|
|
386
|
+
}
|
|
387
|
+
if (['ui','pointer','window','drag','clipboard','launch_app','open_path',
|
|
388
|
+
'reveal_path','notification','multi_select','multi_edit','double_click',
|
|
389
|
+
'right_click','move','down','up','minimize','maximize','restore',
|
|
390
|
+
'move_resize','resize','close'].includes(action)) return true;
|
|
391
|
+
return target !== 'browser';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Keep native desktop tools unavailable in Windows Session 0 without blocking
|
|
395
|
+
// terminal, filesystem and explicitly browser-targeted CDP actions.
|
|
396
|
+
const nativeDesktopTools = [
|
|
397
|
+
'list_windows', 'window_action', 'launch_app', 'ui_snapshot', 'ui_find',
|
|
398
|
+
'ui_action', 'type_text', 'keyboard', 'pointer', 'drag_drop', 'scroll',
|
|
399
|
+
'wait_for_ui', 'clipboard', 'display_inventory', 'screenshot_region',
|
|
400
|
+
'open_path', 'reveal_path', 'notification',
|
|
401
|
+
];
|
|
402
|
+
for (const name of nativeDesktopTools) {
|
|
403
|
+
desktopHandlers[name] = guardWindowsDesktopTool(desktopHandlers[name]);
|
|
404
|
+
}
|
|
405
|
+
desktopHandlers.computer_action = async (args = {}) => {
|
|
406
|
+
if (computerActionRequiresDesktop(args)) await assertWindowsInteractiveDesktop();
|
|
407
|
+
return computerAction(args);
|
|
408
|
+
};
|
|
@@ -8,6 +8,7 @@ import { clamp, desktopSessionAvailable, jsonResult, optionalString, requireEnum
|
|
|
8
8
|
import * as linux from './desktop-linux.mjs';
|
|
9
9
|
import * as macos from './desktop-macos.mjs';
|
|
10
10
|
import * as windows from './desktop-windows.mjs';
|
|
11
|
+
import { windowsDesktopSessionStatus } from './desktop-windows-session.mjs';
|
|
11
12
|
import { browserAction, browserCapabilityAvailable, browserSnapshot } from './browser.mjs';
|
|
12
13
|
import { probeBrowserReadiness } from './browser-readiness.mjs';
|
|
13
14
|
import { findOcrText, ocrImage } from './ocr.mjs';
|
|
@@ -43,8 +44,19 @@ export async function settleComputerSnapshotProbes(tasks, {
|
|
|
43
44
|
return results;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
export async function
|
|
47
|
-
|
|
47
|
+
export async function resolveComputerDesktopSession({
|
|
48
|
+
platform = process.platform,
|
|
49
|
+
available = desktopSessionAvailable(),
|
|
50
|
+
windowsProbe = windowsDesktopSessionStatus,
|
|
51
|
+
} = {}) {
|
|
52
|
+
if (!available) return { status:'unavailable', available:false, reason:'no_graphical_session' };
|
|
53
|
+
if (platform !== 'win32') return { status:'available', available:true };
|
|
54
|
+
return windowsProbe();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function computerSnapshot(args = {}, { desktopSessionProbe = resolveComputerDesktopSession } = {}) {
|
|
58
|
+
const desktopSession = await desktopSessionProbe();
|
|
59
|
+
const desktopAvailable = desktopSession.available !== false;
|
|
48
60
|
const screenshotRequested = args.include_screenshot !== false || args.include_ocr === true;
|
|
49
61
|
const captureScreenshot = desktopAvailable && screenshotRequested;
|
|
50
62
|
const includeUi = args.include_ui !== false;
|
|
@@ -86,7 +98,7 @@ export async function computerSnapshot(args = {}) {
|
|
|
86
98
|
shapeErrors.push({ source:'cursor', message:'cursor position returned an invalid result shape' });
|
|
87
99
|
}
|
|
88
100
|
const parsedUi = includeUi ? parse(uiResult, null) : null;
|
|
89
|
-
const ui = objectRecord(parsedUi) || { platform:process.platform, count:0, nodes:[], ...(includeUi ? { unavailable:true } : { skipped:true }) };
|
|
101
|
+
const ui = objectRecord(parsedUi) || { platform:process.platform, count:0, nodes:[], ...(includeUi ? { unavailable:true, ...(desktopSession.available === false ? { reason:desktopSession.reason } : {}) } : { skipped:true }) };
|
|
90
102
|
if (includeUi && parsedUi !== ui && parsedUi != null) {
|
|
91
103
|
shapeErrors.push({ source:'ui', message:'accessibility snapshot returned an invalid result shape' });
|
|
92
104
|
}
|
|
@@ -208,6 +220,7 @@ export async function computerSnapshot(args = {}) {
|
|
|
208
220
|
device: { hostname: os.hostname(), platform: process.platform, arch: process.arch },
|
|
209
221
|
platform: process.platform,
|
|
210
222
|
hostname: os.hostname(),
|
|
223
|
+
desktop_session:desktopSession,
|
|
211
224
|
active_app: activeApp,
|
|
212
225
|
active_window: activeWindow,
|
|
213
226
|
windows,
|
|
@@ -218,6 +231,7 @@ export async function computerSnapshot(args = {}) {
|
|
|
218
231
|
...(args.browser_readiness === true ? { browser_readiness:readiness || { status:'invalid_response', ready:false, candidate_pages:0, next_step:'use_native_accessibility' } } : {}),
|
|
219
232
|
...(ocr ? { ocr } : {}),
|
|
220
233
|
fallback_chain: {
|
|
234
|
+
desktop_session:desktopSession,
|
|
221
235
|
accessibility: { requested:includeUi, available:includeUi && desktopAvailable && uiResult.status === 'fulfilled' && Boolean(uiResult.value), nodes:uiNodes.length },
|
|
222
236
|
native_browser_dom: {
|
|
223
237
|
requested:includeUi && args.ui_browser_dom === true,
|
|
@@ -34,9 +34,16 @@ export function webRtcWorkerAvailable(env=process.env,platform=process.platform)
|
|
|
34
34
|
export function createDesktopWebRtcBridge({
|
|
35
35
|
env=process.env,platform=process.platform,spawnChild=spawn,
|
|
36
36
|
now=Date.now,setIntervalFn=setInterval,clearIntervalFn=clearInterval,
|
|
37
|
+
stopExitTimeoutMs=5_000,terminationEscalationMs=3_000,
|
|
37
38
|
}={}){
|
|
39
|
+
if(!Number.isSafeInteger(stopExitTimeoutMs)||stopExitTimeoutMs<100||
|
|
40
|
+
stopExitTimeoutMs>10_000||
|
|
41
|
+
!Number.isSafeInteger(terminationEscalationMs)||
|
|
42
|
+
terminationEscalationMs<100||terminationEscalationMs>10_000)
|
|
43
|
+
throw new RangeError('Invalid WebRTC native Stop exit deadline');
|
|
38
44
|
let child=null, current=null, active=false, started=0, seen=0;
|
|
39
|
-
let stdout='', sequence=0, clock=null;
|
|
45
|
+
let stdout='', sequence=0, clock=null, pendingExit=null;
|
|
46
|
+
let lastExitedSession=null;
|
|
40
47
|
const waits=new Map();
|
|
41
48
|
|
|
42
49
|
function rejectWaits(){
|
|
@@ -46,14 +53,45 @@ export function createDesktopWebRtcBridge({
|
|
|
46
53
|
}
|
|
47
54
|
waits.clear();
|
|
48
55
|
}
|
|
49
|
-
|
|
56
|
+
// A SIGTERM request or stdio Stop ACK is NOT evidence of process exit.
|
|
57
|
+
// Hold a local admission fence until the kernel reports the child exit.
|
|
58
|
+
// This covers the direct child only, not escaped descendants: production
|
|
59
|
+
// still requires separate verified process-tree containment.
|
|
60
|
+
function shutdown(alreadyExited=false){
|
|
50
61
|
if(clock){clearIntervalFn(clock);clock=null;}
|
|
51
|
-
const target=child;
|
|
62
|
+
const target=child,session=current;
|
|
52
63
|
child=null;current=null;active=false;stdout='';
|
|
53
64
|
rejectWaits();
|
|
54
|
-
if(target
|
|
55
|
-
|
|
65
|
+
if(!target)return pendingExit?.promise??Promise.resolve(false);
|
|
66
|
+
let confirmExit;
|
|
67
|
+
const exited=new Promise(resolve=>{confirmExit=resolve;});
|
|
68
|
+
const marker={target,promise:exited};
|
|
69
|
+
pendingExit=marker;
|
|
70
|
+
let escalation;
|
|
71
|
+
const onExit=()=>{
|
|
72
|
+
target.removeListener('exit',onExit);
|
|
73
|
+
if(escalation)clearTimeout(escalation);
|
|
74
|
+
if(pendingExit===marker)pendingExit=null;
|
|
75
|
+
lastExitedSession=session;
|
|
76
|
+
confirmExit(true);
|
|
77
|
+
};
|
|
78
|
+
target.once('exit',onExit);
|
|
79
|
+
if(alreadyExited||target.exitCode!==null&&target.exitCode!==undefined||
|
|
80
|
+
target.signalCode!==null&&target.signalCode!==undefined){
|
|
81
|
+
onExit();
|
|
82
|
+
}else{
|
|
83
|
+
// Only signal the child spawned by this bridge. SIGKILL is a bounded
|
|
84
|
+
// escalation, not a Stop proof: never release the fence without exit.
|
|
85
|
+
escalation=setTimeout(()=>{
|
|
86
|
+
if(pendingExit!==marker)return;
|
|
87
|
+
try{target.kill('SIGKILL');}catch{}
|
|
88
|
+
},terminationEscalationMs);
|
|
89
|
+
escalation.unref?.();
|
|
90
|
+
if(!target.killed){
|
|
91
|
+
try{target.stdin.end();target.kill('SIGTERM');}catch{}
|
|
92
|
+
}
|
|
56
93
|
}
|
|
94
|
+
return exited;
|
|
57
95
|
}
|
|
58
96
|
function receive(chunk){
|
|
59
97
|
stdout+=chunk.toString('utf8');
|
|
@@ -140,14 +178,16 @@ export function createDesktopWebRtcBridge({
|
|
|
140
178
|
child=worker;stdout='';
|
|
141
179
|
worker.stdout.on('data',chunk=>{if(child===worker)receive(chunk);});
|
|
142
180
|
worker.once('error',()=>{if(child===worker)shutdown();});
|
|
143
|
-
worker.once('exit',()=>{if(child===worker)shutdown();});
|
|
181
|
+
worker.once('exit',()=>{if(child===worker)shutdown(true);});
|
|
144
182
|
}
|
|
145
183
|
|
|
146
184
|
async function offer({session_id:sid,mode,sdp},extra={}){
|
|
147
185
|
if(!UUID.test(String(sid))||mode!=='view'||typeof sdp!=='string'||
|
|
148
186
|
Buffer.byteLength(sdp)>MAX_SDP||sdp.length<100)
|
|
149
187
|
throw new Error('Invalid view-only WebRTC offer');
|
|
150
|
-
if(current!==null||child)
|
|
188
|
+
if(current!==null||child||pendingExit)
|
|
189
|
+
throw new Error('Another WebRTC session is active');
|
|
190
|
+
lastExitedSession=null;
|
|
151
191
|
current=sid;started=seen=now();active=false;
|
|
152
192
|
let cancelled=false;
|
|
153
193
|
const cancel=()=>{cancelled=true;shutdown();};
|
|
@@ -189,10 +229,19 @@ export function createDesktopWebRtcBridge({
|
|
|
189
229
|
try{
|
|
190
230
|
const reply=await send('stop',sid,{},4000);
|
|
191
231
|
if(reply.sessionId!==sid)throw new Error('Invalid WebRTC stop acknowledgement');
|
|
192
|
-
|
|
193
|
-
|
|
232
|
+
}catch(error){shutdown();throw error;}
|
|
233
|
+
const exited=shutdown();
|
|
234
|
+
let timer;
|
|
235
|
+
const confirmed=lastExitedSession===sid||await Promise.race([
|
|
236
|
+
exited,
|
|
237
|
+
new Promise(resolve=>{timer=setTimeout(()=>resolve(false),stopExitTimeoutMs);}),
|
|
238
|
+
]).finally(()=>clearTimeout(timer));
|
|
239
|
+
if(!confirmed)
|
|
240
|
+
throw new Error('WebRTC worker Stop unconfirmed: native process still running');
|
|
241
|
+
return jsonResult({ok:true,sessionId:sid});
|
|
194
242
|
}
|
|
195
|
-
|
|
243
|
+
// Do not expose the internal alreadyExited override to callers.
|
|
244
|
+
return {offer,heartbeat,stop,shutdown:()=>shutdown(),
|
|
196
245
|
get activeSession(){return current;},
|
|
197
246
|
};
|
|
198
247
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Windows native UI runs in a user's interactive session, never in Session 0.
|
|
2
|
+
// This only examines the current probe process; it does not enumerate users,
|
|
3
|
+
// request a login, open the secure desktop or change Windows permissions.
|
|
4
|
+
import { runPowerShell } from './common.mjs';
|
|
5
|
+
|
|
6
|
+
export const WINDOWS_DESKTOP_PROBE = String.raw`
|
|
7
|
+
$ErrorActionPreference='Stop'
|
|
8
|
+
$p=[System.Diagnostics.Process]::GetCurrentProcess()
|
|
9
|
+
[pscustomobject]@{
|
|
10
|
+
sessionId=$p.SessionId
|
|
11
|
+
userInteractive=[Environment]::UserInteractive
|
|
12
|
+
} | ConvertTo-Json -Compress
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export function classifyWindowsDesktopSession(value) {
|
|
16
|
+
const sessionId = value?.sessionId;
|
|
17
|
+
const interactive = value?.userInteractive;
|
|
18
|
+
if (!Number.isSafeInteger(sessionId) || sessionId < 0 || typeof interactive !== 'boolean') {
|
|
19
|
+
return { status:'unknown', available:null, reason:'invalid_session_probe' };
|
|
20
|
+
}
|
|
21
|
+
if (sessionId === 0 || !interactive) {
|
|
22
|
+
return {
|
|
23
|
+
status:'unavailable',
|
|
24
|
+
available:false,
|
|
25
|
+
reason:sessionId === 0 ? 'windows_session_0' : 'noninteractive_process',
|
|
26
|
+
next_step:'sign_in_to_interactive_desktop',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { status:'available', available:true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function windowsDesktopSessionStatus({ runPowerShellFn = runPowerShell } = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const result = await runPowerShellFn(WINDOWS_DESKTOP_PROBE, {
|
|
35
|
+
label:'Windows interactive desktop readiness',
|
|
36
|
+
timeout:6000,
|
|
37
|
+
});
|
|
38
|
+
return classifyWindowsDesktopSession(JSON.parse(String(result?.stdout || '').trim()));
|
|
39
|
+
} catch {
|
|
40
|
+
// A failed OS query is not proof that no user is signed in.
|
|
41
|
+
return { status:'unknown', available:null, reason:'session_probe_failed' };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Tool discovery can request the same session status several times per second.
|
|
46
|
+
// Cache only advertised capabilities; native actions always use a fresh probe.
|
|
47
|
+
export function createWindowsDesktopSessionCache({
|
|
48
|
+
probe = windowsDesktopSessionStatus,
|
|
49
|
+
ttlMs = 2500,
|
|
50
|
+
nowFn = Date.now,
|
|
51
|
+
} = {}) {
|
|
52
|
+
let cached = null;
|
|
53
|
+
let expiresAt = 0;
|
|
54
|
+
let inFlight = null;
|
|
55
|
+
return async () => {
|
|
56
|
+
if (cached && nowFn() < expiresAt) return cached;
|
|
57
|
+
if (inFlight) return inFlight;
|
|
58
|
+
inFlight = Promise.resolve().then(probe).catch(() => ({
|
|
59
|
+
status:'unknown', available:null, reason:'session_probe_failed',
|
|
60
|
+
})).then(result => {
|
|
61
|
+
cached = result && typeof result.available === 'boolean'
|
|
62
|
+
? result
|
|
63
|
+
: { status:'unknown', available:null, reason:'session_probe_failed' };
|
|
64
|
+
expiresAt = nowFn() + Math.max(0, ttlMs);
|
|
65
|
+
return cached;
|
|
66
|
+
}).finally(() => { inFlight = null; });
|
|
67
|
+
return inFlight;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const cachedWindowsDesktopSessionStatus = createWindowsDesktopSessionCache();
|
|
72
|
+
|
|
73
|
+
export async function assertWindowsInteractiveDesktop({
|
|
74
|
+
platform = process.platform,
|
|
75
|
+
probe = windowsDesktopSessionStatus,
|
|
76
|
+
} = {}) {
|
|
77
|
+
if (platform !== 'win32') return;
|
|
78
|
+
const status = await probe();
|
|
79
|
+
if (status?.available === true) return;
|
|
80
|
+
const reason = status?.reason || 'session_probe_failed';
|
|
81
|
+
throw new Error(`Windows desktop unavailable (${reason}): sign in to an interactive Windows desktop before using native screen or input tools.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function guardWindowsDesktopTool(handler, options = {}) {
|
|
85
|
+
return async (...args) => {
|
|
86
|
+
await assertWindowsInteractiveDesktop(options);
|
|
87
|
+
return handler(...args);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -9,6 +9,7 @@ import { isWaylandSession } from '../screenshot-portal.mjs';
|
|
|
9
9
|
import { openDirectoryPath } from '../tools/files.mjs';
|
|
10
10
|
import { resolveSafePath } from '../util.mjs';
|
|
11
11
|
import { clamp, commandExists, jsonResult, runFile, unavailable } from './common.mjs';
|
|
12
|
+
import { assertWindowsInteractiveDesktop } from './desktop-windows-session.mjs';
|
|
12
13
|
|
|
13
14
|
export function resolveRecordScreenFfmpeg({
|
|
14
15
|
platform = process.platform,
|
|
@@ -330,6 +331,8 @@ async function copyRecordingFile(source, destination) {
|
|
|
330
331
|
}
|
|
331
332
|
|
|
332
333
|
export async function recordScreen(args) {
|
|
334
|
+
// Reject Windows Session 0 before creating temporary files or starting ffmpeg.
|
|
335
|
+
await assertWindowsInteractiveDesktop();
|
|
333
336
|
const seconds = clamp(args.duration_seconds, 5, 1, 120);
|
|
334
337
|
const fps = clamp(args.fps, 15, 1, 60);
|
|
335
338
|
const wayland = process.platform === 'linux' && isWaylandSession();
|
package/src/index.mjs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { advertisedTools as advertisedCoreTools,
|
|
6
|
-
import { allExtendedTools,
|
|
5
|
+
import { advertisedTools as advertisedCoreTools, toolDefinitions as coreToolDefinitions } from './catalog.mjs';
|
|
6
|
+
import { allExtendedTools, extendedToolDefinitions } from './extended/catalog.mjs';
|
|
7
|
+
import { supportedRuntimeToolDefinitions } from './tool-availability.mjs';
|
|
7
8
|
import { describeConfig, configurationError, runtimeConfigDir } from './config.mjs';
|
|
8
9
|
import { shutdownSessions, startSessionSweeper } from './sessions.mjs';
|
|
9
10
|
import { flush, setTelemetrySink, shutdownTelemetry, telemetryEnabled } from './telemetry.mjs';
|
|
@@ -13,7 +14,8 @@ const args = process.argv.slice(2);
|
|
|
13
14
|
const allToolDefinitions = [...coreToolDefinitions, ...extendedToolDefinitions];
|
|
14
15
|
|
|
15
16
|
async function advertisedRuntimeTools() {
|
|
16
|
-
|
|
17
|
+
const { core, extended } = await supportedRuntimeToolDefinitions();
|
|
18
|
+
return [...core, ...extended];
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
async function writeStdout(value) {
|
package/src/mcp-v2.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { toolDefinitions, TEXT_OUTPUT_SCHEMA } from './catalog.mjs';
|
|
3
|
+
import { extendedToolDefinitions } from './extended/catalog.mjs';
|
|
4
|
+
import { supportedRuntimeToolDefinitions } from './tool-availability.mjs';
|
|
4
5
|
import { invokeTool } from './invoke.mjs';
|
|
5
6
|
|
|
6
7
|
const DEFAULT_CAPABILITY_POLL_MS = 3000;
|
|
@@ -26,10 +27,8 @@ export async function startRuntimeMcpServer({ version, instructions, onError = (
|
|
|
26
27
|
import('@modelcontextprotocol/server/stdio'),
|
|
27
28
|
]);
|
|
28
29
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
...(await advertisedExtendedTools()).map(tool => tool.name),
|
|
32
|
-
]);
|
|
30
|
+
const initial = await supportedRuntimeToolDefinitions();
|
|
31
|
+
const initiallySupported = new Set([...initial.core, ...initial.extended].map(tool => tool.name));
|
|
33
32
|
let activeServer = null;
|
|
34
33
|
let capabilityTimer = null;
|
|
35
34
|
let capabilityPollRunning = false;
|
|
@@ -69,9 +68,10 @@ export async function startRuntimeMcpServer({ version, instructions, onError = (
|
|
|
69
68
|
if (closed || capabilityPollRunning) return;
|
|
70
69
|
capabilityPollRunning = true;
|
|
71
70
|
try {
|
|
72
|
-
const
|
|
71
|
+
const updated = await supportedRuntimeToolDefinitions();
|
|
72
|
+
const next = new Set([...updated.core, ...updated.extended].map(tool => tool.name));
|
|
73
73
|
let changed = false;
|
|
74
|
-
for (const definition of extendedToolDefinitions) {
|
|
74
|
+
for (const definition of [...toolDefinitions, ...extendedToolDefinitions]) {
|
|
75
75
|
const before = lastSupported.has(definition.name);
|
|
76
76
|
const after = next.has(definition.name);
|
|
77
77
|
if (before === after) continue;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { supportedCoreTools } from './catalog.mjs';
|
|
3
|
+
import { advertisedExtendedTools, capabilitySnapshot } from './extended/catalog.mjs';
|
|
4
|
+
|
|
5
|
+
// The core screenshot tool is distinct from extended screenshot_region, but
|
|
6
|
+
// both require a real Windows user desktop. Keep read_image and all other
|
|
7
|
+
// filesystem tools available to Session 0 service agents.
|
|
8
|
+
export function filterCoreToolsForLiveDesktop(tools, {
|
|
9
|
+
platform = process.platform,
|
|
10
|
+
capabilities = {},
|
|
11
|
+
} = {}) {
|
|
12
|
+
if (platform !== 'win32' || capabilities.desktop_session === true) return tools;
|
|
13
|
+
return tools.filter(tool => tool.name !== 'take_screenshot');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function supportedRuntimeToolDefinitions() {
|
|
17
|
+
// One consistent OS capability snapshot for ordinary and compact MCP lists.
|
|
18
|
+
const capabilities = await capabilitySnapshot();
|
|
19
|
+
return {
|
|
20
|
+
core:filterCoreToolsForLiveDesktop(supportedCoreTools(), { capabilities }),
|
|
21
|
+
extended:await advertisedExtendedTools(capabilities),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -11,6 +11,7 @@ import { documentKind, readDocxText, readPdfText } from '../documents.mjs';
|
|
|
11
11
|
import { diffStats, unifiedDiff } from '../diff.mjs';
|
|
12
12
|
import { describeFilesystemFailure } from '../permissions.mjs';
|
|
13
13
|
import { capturePortalScreenshot, isWaylandSession } from '../screenshot-portal.mjs';
|
|
14
|
+
import { assertWindowsInteractiveDesktop } from '../extended/desktop-windows-session.mjs';
|
|
14
15
|
import { applyHunks, parseUnifiedDiff } from '../patch.mjs';
|
|
15
16
|
import { countEvent, recordEvent } from '../telemetry.mjs';
|
|
16
17
|
import { clampInteger, decodeText, displayPath, fail, fitsOutput, globToRegExp, image, isInsideRoot, looksBinary, multi, pageLines, resolveSafePath, splitLines, text, throwIfCancelled } from '../util.mjs';
|
|
@@ -98,6 +99,8 @@ export async function runWindowsScreenshot(file, runProcess = execFileAsync) {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
export async function takeScreenshotTool(args) {
|
|
102
|
+
// Reject Windows Session 0 before allocating files or running pixel capture.
|
|
103
|
+
await assertWindowsInteractiveDesktop();
|
|
101
104
|
const requestedDirectory = args.directory ? await resolveSafePath(args.directory, 'directory') : '';
|
|
102
105
|
const temporaryDirectory = requestedDirectory ? '' : await mkdtemp(path.join(os.tmpdir(), 'remcp-screenshot-'));
|
|
103
106
|
const directory = requestedDirectory || temporaryDirectory;
|