@remcp/remcp 0.2.223 → 0.2.225
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remcp/remcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.225",
|
|
4
4
|
"description": "ReMCP device client: pair a computer with ReMCP and run the outbound-only agent that hosts the local MCP runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"check": "node --check bin/remcp.mjs && node --check src/agent-deploy-reconnect.mjs && node --check src/agent-network-probe-client.mjs && node --check src/agent-relay-connection.mjs && node --check src/agent-responder.mjs && node --check src/agent-runtime-capabilities.mjs && node --check src/agent-update-monitor.mjs && node --check src/agent-update.mjs && node --check src/agent-utilities.mjs && node --check src/agent.mjs && node --check src/cli.mjs && node --check src/cli/config.mjs && node --check src/cli/connect.mjs && node --check src/cli/doctor.mjs && node --check src/cli/env.mjs && node --check src/cli/installation-audit.mjs && node --check src/cli/mac-native-temperature.mjs && node --check src/cli/npm-global.mjs && node --check src/cli/pairing-qr.mjs && node --check src/cli/service-inventory.mjs && node --check src/cli/service-macos.mjs && node --check src/cli/service-paths.mjs && node --check src/cli/service.mjs && node --check src/cli/shell.mjs && node --check src/cli/update.mjs && node --check src/cli/windows-owned-job-source.mjs && node --check src/cli/windows-owned-job.mjs && node --check src/cli/windows-supervisor.mjs && node --check src/cli/windows-
|
|
21
|
+
"check": "node --check bin/remcp.mjs && node --check src/agent-deploy-reconnect.mjs && node --check src/agent-network-probe-client.mjs && node --check src/agent-relay-connection.mjs && node --check src/agent-responder.mjs && node --check src/agent-runtime-capabilities.mjs && node --check src/agent-update-monitor.mjs && node --check src/agent-update.mjs && node --check src/agent-utilities.mjs && node --check src/agent.mjs && node --check src/cli.mjs && node --check src/cli/config.mjs && node --check src/cli/connect.mjs && node --check src/cli/doctor.mjs && node --check src/cli/env.mjs && node --check src/cli/installation-audit.mjs && node --check src/cli/mac-native-temperature.mjs && node --check src/cli/npm-global.mjs && node --check src/cli/pairing-qr.mjs && node --check src/cli/service-inventory.mjs && node --check src/cli/service-macos.mjs && node --check src/cli/service-paths.mjs && node --check src/cli/service.mjs && node --check src/cli/shell.mjs && node --check src/cli/update.mjs && node --check src/cli/windows-owned-job-source.mjs && node --check src/cli/windows-owned-job.mjs && node --check src/cli/windows-supervisor.mjs && node --check src/cli/windows-update-lock.mjs && node --check src/cli/windows-user-supervisor.mjs && node --check src/fetch-update-manifest.mjs && node --check src/fs-access.mjs && node --check src/npm.mjs && node --check src/runtime.mjs && node --check src/version.mjs",
|
|
22
22
|
"test": "node --test test/*.test.mjs"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
package/src/cli/service.mjs
CHANGED
|
@@ -15,11 +15,10 @@ export { macLaunchDomain, ensureMacCliCommand, installMacService } from './servi
|
|
|
15
15
|
import { PACKAGE_NAME, VERSION } from '../version.mjs';
|
|
16
16
|
|
|
17
17
|
import { saveConfig } from './config.mjs';
|
|
18
|
-
import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macServiceFile, macServiceLabel, windowsServiceLauncherFile,
|
|
18
|
+
import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macServiceFile, macServiceLabel, windowsServiceLauncherFile, windowsStartupScriptFile, windowsTaskName } from './env.mjs';
|
|
19
19
|
import { run } from './shell.mjs';
|
|
20
|
-
import { windowsServiceLauncherText,
|
|
20
|
+
import { windowsServiceLauncherText, windowsTaskAbsenceCommand, windowsStartupScriptText, windowsStartupScriptInstalled } from './windows-supervisor.mjs';
|
|
21
21
|
import { ensureWindowsSupervisorKey, windowsSupervisorCommand } from './windows-user-supervisor.mjs';
|
|
22
|
-
import { scheduleWindowsTaskRestart } from './windows-task-restart.mjs';
|
|
23
22
|
|
|
24
23
|
export function quoteSystemd(value) {
|
|
25
24
|
return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
@@ -79,20 +78,6 @@ export function writeWindowsServiceLauncher(cliPath = globalCliPath(), nodePath
|
|
|
79
78
|
return windowsServiceLauncherFile;
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
function writeWindowsTaskScript(launcherFile) {
|
|
83
|
-
if (fs.existsSync(windowsTaskScriptFile) && !windowsTaskScriptInstalled(windowsTaskScriptFile, windowsServiceLauncherFile)) {
|
|
84
|
-
throw new Error('The ReMCP scheduled task script is not owned by ReMCP; refusing to overwrite it');
|
|
85
|
-
}
|
|
86
|
-
const script = windowsTaskScriptText(launcherFile);
|
|
87
|
-
fs.mkdirSync(configDir, { recursive:true, mode:0o700 });
|
|
88
|
-
const temporary = `${windowsTaskScriptFile}.${process.pid}.${Date.now()}.tmp`;
|
|
89
|
-
try {
|
|
90
|
-
fs.writeFileSync(temporary, '\uFEFF' + script, { flag:'wx', encoding:'utf16le', mode:0o600 });
|
|
91
|
-
fs.renameSync(temporary, windowsTaskScriptFile);
|
|
92
|
-
} finally { try { fs.unlinkSync(temporary); } catch {} }
|
|
93
|
-
return windowsTaskScriptFile;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
81
|
function installWindowsUserStartup(cliPath, nodePath) {
|
|
97
82
|
const existed = fs.existsSync(windowsStartupScriptFile);
|
|
98
83
|
if (existed && !windowsStartupScriptInstalled(windowsStartupScriptFile)) {
|
|
@@ -140,64 +125,39 @@ function installWindowsUserStartup(cliPath, nodePath) {
|
|
|
140
125
|
console.log('ReMCP uses this Windows account\'s Startup folder (no administrator rights required).');
|
|
141
126
|
}
|
|
142
127
|
|
|
143
|
-
function
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
const
|
|
128
|
+
function windowsScheduledTaskPresence() {
|
|
129
|
+
// Never interpret schtasks.exe failure as absence: it also reports access-denied.
|
|
130
|
+
// COM returns locale-independent PRESENT/ABSENT or throws on an uncertain result.
|
|
131
|
+
const query = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive',
|
|
147
132
|
'-Command', windowsTaskAbsenceCommand(windowsTaskName)],
|
|
148
|
-
{ encoding:'utf8', windowsHide:true, timeout:
|
|
149
|
-
|
|
150
|
-
|
|
133
|
+
{ encoding:'utf8', windowsHide:true, timeout:30000, maxBuffer:4096, input:'' });
|
|
134
|
+
const state = String(query.stdout || '').trim();
|
|
135
|
+
if (query.error || query.status !== 0 || !['ABSENT', 'PRESENT'].includes(state)) {
|
|
136
|
+
throw new Error('Could not verify the legacy ReMCP Scheduled Task; refusing an unsafe lifecycle change');
|
|
151
137
|
}
|
|
138
|
+
return state;
|
|
152
139
|
}
|
|
153
140
|
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (windowsBatchUnsafeProfile(cliPath, nodePath, windowsServiceLauncherFile)) {
|
|
160
|
-
requireWindowsScheduledTaskAbsent();
|
|
161
|
-
console.log('ReMCP: Unicode or shell-sensitive Windows profile; using the per-user Startup supervisor.');
|
|
162
|
-
installWindowsUserStartup(cliPath, nodePath);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
const launcherFile = writeWindowsServiceLauncher(cliPath, nodePath);
|
|
166
|
-
const taskScript = writeWindowsTaskScript(launcherFile);
|
|
167
|
-
// WScript has no console; its hidden CMD child remains attached until it exits.
|
|
168
|
-
// /IT keeps desktop/UI automation in the signed-in user's session.
|
|
169
|
-
const command = `wscript.exe //B //Nologo "${taskScript}"`;
|
|
170
|
-
// Current-user interactive tasks need /IT. /RL LIMITED avoids requiring elevation.
|
|
171
|
-
// An organization may still disable creation of all tasks by standard users.
|
|
172
|
-
const created = spawnSync('schtasks.exe',
|
|
173
|
-
['/Create', '/TN', windowsTaskName, '/TR', command, '/SC', 'ONLOGON', '/RL', 'LIMITED', '/IT', '/F'],
|
|
174
|
-
{ encoding:'utf8', timeout:15000, maxBuffer:64 * 1024, windowsHide:true, input:'' });
|
|
175
|
-
if (created.error || created.status !== 0) {
|
|
176
|
-
requireWindowsScheduledTaskAbsent();
|
|
177
|
-
console.warn('Windows Task Scheduler rejected task creation; switching to per-user Startup.');
|
|
178
|
-
installWindowsUserStartup(cliPath, nodePath);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
// Recovery tuning is optional; a policy denying Set-ScheduledTask must not invalidate
|
|
182
|
-
// a successfully created and runnable interactive task.
|
|
183
|
-
try {
|
|
184
|
-
run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', windowsTaskReliabilityCommand(windowsTaskName)]);
|
|
185
|
-
} catch (error) {
|
|
186
|
-
console.warn(`ReMCP task recovery settings were not applied: ${error.message}`);
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
189
|
-
run('schtasks.exe', ['/Run', '/TN', windowsTaskName], { timeout:15000 });
|
|
190
|
-
} catch (error) {
|
|
191
|
-
// We just created this task. Remove it before registering another launcher, so
|
|
192
|
-
// a delayed logon cannot start competing agents under two supervisors.
|
|
193
|
-
const removed = spawnSync('schtasks.exe', ['/Delete', '/TN', windowsTaskName, '/F'],
|
|
194
|
-
{ stdio:'ignore', timeout:10000 });
|
|
195
|
-
if (removed.status !== 0) throw error;
|
|
196
|
-
console.warn('Windows Task Scheduler could not start ReMCP; using per-user Startup.');
|
|
197
|
-
installWindowsUserStartup(cliPath, nodePath);
|
|
141
|
+
function requireWindowsScheduledTaskAbsent() {
|
|
142
|
+
if (windowsScheduledTaskPresence() !== 'ABSENT') {
|
|
143
|
+
throw new Error('A legacy ReMCP Scheduled Task exists; its child process tree cannot be safely stopped or migrated automatically');
|
|
198
144
|
}
|
|
199
145
|
}
|
|
200
146
|
|
|
147
|
+
// Called before any npm install/update so a legacy task cannot be partially
|
|
148
|
+
// replaced while its WScript/CMD/Node descendants are still running.
|
|
149
|
+
export function assertWindowsUpdateLifecycleSafe() {
|
|
150
|
+
if (servicePlatform() === 'win32') requireWindowsScheduledTaskAbsent();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function installWindowsService(cliPath = globalCliPath(), { nodePath = process.execPath } = {}) {
|
|
154
|
+
// The old Task Scheduler launcher (WScript -> CMD -> Node) can orphan its
|
|
155
|
+
// descendants after /End. Do not create or touch it on new installations.
|
|
156
|
+
// An existing task must be migrated with verified process ownership first.
|
|
157
|
+
requireWindowsScheduledTaskAbsent();
|
|
158
|
+
installWindowsUserStartup(cliPath, nodePath);
|
|
159
|
+
}
|
|
160
|
+
|
|
201
161
|
// `serviceInstalled` was added after background services already existed in the wild. Treat an
|
|
202
162
|
// explicit false as the user's opt-out, an explicit true as the current marker, and only infer the
|
|
203
163
|
// old intent from an OS service artifact when the marker is absent. This keeps legacy installs
|
|
@@ -382,20 +342,14 @@ export function restartPersistentServiceIfInstalled(config, { deferLinux = false
|
|
|
382
342
|
return macServiceLabel;
|
|
383
343
|
}
|
|
384
344
|
if (platform === 'win32') {
|
|
345
|
+
// /End can leave Node descendants running after a legacy CMD task ends.
|
|
346
|
+
// Do not report a successful credential restart when ownership is unknown.
|
|
347
|
+
requireWindowsScheduledTaskAbsent();
|
|
385
348
|
if (windowsStartupScriptInstalled(windowsStartupScriptFile)) {
|
|
386
349
|
const state = windowsSupervisorCommand('restart');
|
|
387
350
|
if (state.restartScheduled !== true) throw new Error('Windows user supervisor did not schedule a restart');
|
|
388
351
|
return 'ReMCP user startup supervisor (restart scheduled)';
|
|
389
352
|
}
|
|
390
|
-
const result = spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName], { stdio: 'ignore' });
|
|
391
|
-
if (result.status === 0) {
|
|
392
|
-
if (deferWindows) return scheduleWindowsTaskRestart(canonicalServiceNodePath(config));
|
|
393
|
-
// /Run does not restart a task that is already running; stop the previous
|
|
394
|
-
// process first so a rotated credential is loaded by the replacement.
|
|
395
|
-
spawnSync('schtasks.exe', ['/End', '/TN', windowsTaskName], { stdio: 'ignore' });
|
|
396
|
-
run('schtasks.exe', ['/Run', '/TN', windowsTaskName]);
|
|
397
|
-
return windowsTaskName;
|
|
398
|
-
}
|
|
399
353
|
}
|
|
400
354
|
return null;
|
|
401
355
|
}
|
|
@@ -412,6 +366,13 @@ export function uninstallPersistentService() {
|
|
|
412
366
|
try { fs.unlinkSync(macServiceFile); } catch {}
|
|
413
367
|
removeMacCliCommand();
|
|
414
368
|
} else if (platform === 'win32') {
|
|
369
|
+
// Refuse before touching any artifact. Scheduler /End and /Delete cannot
|
|
370
|
+
// prove that its detached CMD/Node descendants exited.
|
|
371
|
+
requireWindowsScheduledTaskAbsent();
|
|
372
|
+
if (fs.existsSync(windowsStartupScriptFile) &&
|
|
373
|
+
!windowsStartupScriptInstalled(windowsStartupScriptFile)) {
|
|
374
|
+
throw new Error('The ReMCP Startup entry is not owned by ReMCP; refusing to remove it');
|
|
375
|
+
}
|
|
415
376
|
if (windowsStartupScriptInstalled(windowsStartupScriptFile)) {
|
|
416
377
|
// Removing Startup alone leaves the already spawned agent alive. Stop the
|
|
417
378
|
// exact child owned by the authenticated singleton, then remove registration.
|
|
@@ -426,9 +387,7 @@ export function uninstallPersistentService() {
|
|
|
426
387
|
}
|
|
427
388
|
fs.unlinkSync(windowsStartupScriptFile);
|
|
428
389
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
try { if (windowsTaskScriptInstalled(windowsTaskScriptFile, windowsServiceLauncherFile)) fs.unlinkSync(windowsTaskScriptFile); } catch {}
|
|
432
|
-
try { fs.unlinkSync(windowsServiceLauncherFile); } catch {}
|
|
390
|
+
// Retain legacy launch files. Their presence alone is not proof of ownership,
|
|
391
|
+
// and an absent task provides no safe justification for deleting them.
|
|
433
392
|
}
|
|
434
393
|
}
|
package/src/cli/update.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { loadConfig, saveConfig } from './config.mjs';
|
|
|
12
12
|
import { assertSecureServerUrl } from './connect.mjs';
|
|
13
13
|
import { installedVersion } from './doctor.mjs';
|
|
14
14
|
import { linuxServiceFile, macServiceFile, officialOrigin } from './env.mjs';
|
|
15
|
-
import { currentInstallationInfo, ensureMacCliCommand, ensureServiceIfRecorded, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, servicePlatform, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
|
|
15
|
+
import { assertWindowsUpdateLifecycleSafe, currentInstallationInfo, ensureMacCliCommand, ensureServiceIfRecorded, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, servicePlatform, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
|
|
16
16
|
|
|
17
17
|
const UPDATE_DISCOVERY_TIMEOUT_MS = 5000;
|
|
18
18
|
|
|
@@ -264,6 +264,9 @@ export async function updateCommand(flags) {
|
|
|
264
264
|
return;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
// Legacy Scheduled Task /End can orphan an already running agent. Refuse the
|
|
268
|
+
// update before touching npm or package state until ownership is verifiable.
|
|
269
|
+
if (serviceExpected) assertWindowsUpdateLifecycleSafe();
|
|
267
270
|
const before = { cli: VERSION, runtime: installedVersion(cfg.runtime.packageName) };
|
|
268
271
|
console.log(`Updating ReMCP to ${targets.clientSpec} with ${targets.runtimeSpec}…`);
|
|
269
272
|
npmGlobalUpdate([PACKAGE_NAME, cfg.runtime.packageName], targets.clientSpec, targets.runtimeSpec);
|
|
@@ -21,14 +21,21 @@ export function spawnWindowsOwnedJob(application, args, { cwd, log, env = proces
|
|
|
21
21
|
Buffer.from(windowsOwnedJobScript, 'utf16le').toString('base64')], {
|
|
22
22
|
cwd:path.dirname(powershell), env, windowsHide:true, stdio:['pipe','pipe','pipe'],
|
|
23
23
|
});
|
|
24
|
-
let output = '', stderr = '', started = false, empty = false, settled = false;
|
|
24
|
+
let output = '', stderr = '', started = false, empty = false, settled = false, sawReady = false;
|
|
25
25
|
let resolveReady, rejectReady, resolveDone, rejectDone;
|
|
26
26
|
const ready = new Promise((resolve, reject) => { resolveReady=resolve; rejectReady=reject; });
|
|
27
27
|
const done = new Promise((resolve, reject) => { resolveDone=resolve; rejectDone=reject; });
|
|
28
28
|
// Callers may observe readiness and completion separately; suppress unhandled rejection noise.
|
|
29
29
|
ready.catch(() => {}); done.catch(() => {});
|
|
30
30
|
const fail = error => { rejectReady(error); rejectDone(error); };
|
|
31
|
-
|
|
31
|
+
// Windows PowerShell 5.1 compiles this P/Invoke helper via Add-Type on every launch.
|
|
32
|
+
// Resource-constrained ARM64 VMs can need more than 15 seconds. Keep one bounded
|
|
33
|
+
// deadline for the complete handshake, without weakening job ownership checks.
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
fail(new Error('Windows job startup timed out during ' +
|
|
36
|
+
(sawReady ? 'process creation' : 'PowerShell initialization')));
|
|
37
|
+
helper.kill();
|
|
38
|
+
}, 45000);
|
|
32
39
|
helper.stdin.on('error', () => {});
|
|
33
40
|
helper.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-2048); });
|
|
34
41
|
helper.stdout.on('data', chunk => {
|
|
@@ -39,6 +46,7 @@ export function spawnWindowsOwnedJob(application, args, { cwd, log, env = proces
|
|
|
39
46
|
try {
|
|
40
47
|
const message = JSON.parse(line);
|
|
41
48
|
if (message.ready === true && !started) {
|
|
49
|
+
sawReady = true;
|
|
42
50
|
// ASCII envelope survives Windows PowerShell 5.1's OEM console codepage.
|
|
43
51
|
helper.stdin.write(Buffer.from(JSON.stringify({application,command,cwd,log}),'utf8').toString('base64') + '\n');
|
|
44
52
|
} else if (message.started === true && Number.isSafeInteger(message.pid) && message.pid > 0 && !started) {
|
|
@@ -15,6 +15,11 @@ const root = process.env.REMCP_CONFIG_DIR || path.join(os.homedir(), '.config',
|
|
|
15
15
|
const keyFile = path.join(root, 'windows-startup-control.key');
|
|
16
16
|
const actions = new Set(['status', 'stop', 'restart']);
|
|
17
17
|
|
|
18
|
+
// Startup readiness must cover the owned Job Object helper's 45-second PowerShell
|
|
19
|
+
// initialization budget plus process startup. The synchronous CLI needs extra
|
|
20
|
+
// time to collect the result from the independent control process.
|
|
21
|
+
export const WINDOWS_SUPERVISOR_READY_BUDGET_MS = 57000;
|
|
22
|
+
|
|
18
23
|
export function windowsSupervisorPath(configDir = root, platform = process.platform) {
|
|
19
24
|
const id = crypto.createHash('sha256').update(path.resolve(configDir)).digest('hex').slice(0, 32);
|
|
20
25
|
return platform === 'win32' ? '\\\\.\\pipe\\remcp-user-' + id
|
|
@@ -74,7 +79,10 @@ export async function windowsSupervisorRequest(action, { timeoutMs = action ===
|
|
|
74
79
|
|
|
75
80
|
// Synchronous callers in the installed CLI use a separate Node client.
|
|
76
81
|
// Never pass device tokens or control keys in command-line arguments.
|
|
77
|
-
export function windowsSupervisorCommand(action, {
|
|
82
|
+
export function windowsSupervisorCommand(action, {
|
|
83
|
+
timeoutMs = action === 'stop' ? 20000 :
|
|
84
|
+
action === 'wait' ? WINDOWS_SUPERVISOR_READY_BUDGET_MS + 8000 : 15000,
|
|
85
|
+
} = {}) {
|
|
78
86
|
if (!actions.has(action) && action !== 'wait') throw new Error('Invalid supervisor action');
|
|
79
87
|
const proc = spawnSync(process.execPath, [currentFile, 'control', action], {
|
|
80
88
|
encoding: 'utf8', windowsHide: true, timeout: timeoutMs, maxBuffer: 8192,
|
|
@@ -245,7 +253,7 @@ async function control(action) {
|
|
|
245
253
|
throw new Error('Windows Startup supervisor has not exited after stop');
|
|
246
254
|
}
|
|
247
255
|
if (action !== 'wait') return windowsSupervisorRequest(action);
|
|
248
|
-
const deadline = Date.now() +
|
|
256
|
+
const deadline = Date.now() + WINDOWS_SUPERVISOR_READY_BUDGET_MS;
|
|
249
257
|
while (Date.now() < deadline) {
|
|
250
258
|
try {
|
|
251
259
|
const current = await windowsSupervisorRequest('status', { timeoutMs: 1000 });
|