@remcp/remcp 0.2.207 → 0.2.209

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/README.md CHANGED
@@ -63,7 +63,20 @@ Two things it deliberately does not do:
63
63
  a non-interactive command cannot type one. Nothing in this mode grants root by itself.
64
64
 
65
65
  While it is on, `get_runtime_info` reports `policy.unrestricted: true`, so the model can see it and
66
- say so instead of assuming the guardrails are still there.
66
+ say so instead of assuming the guardrails are still there. The same response includes
67
+ `osPrivilegeBoundary`, reporting effective user and `NoNewPrivs` on Linux, the user boundary on
68
+ macOS, and the actual Windows process integrity level detected via `whoami.exe /groups`.
69
+ If that command is unavailable, Windows elevation remains `unknown`; process integrity
70
+ is not equivalent to blanket administrator access.
71
+
72
+ The owner-managed Linux `remcp-agent.service` does not set `NoNewPrivileges=true`: sudo/polkit
73
+ may request normal OS approval instead of being blocked by systemd. `remcp update` also removes
74
+ this legacy line from older user units. A systemd `daemon-reload` does not change an already
75
+ running process; activating the new privilege boundary requires an agent restart, which can stop
76
+ running terminal sessions and child processes. This does **not** enable passwordless root.
77
+ On macOS, launchd runs as the signed-in user, and TCC controls Full Disk Access and Accessibility.
78
+ On Windows, the interactive scheduled task requests the highest privileges available to its
79
+ principal; UAC and the actual process token remain authoritative.
67
80
 
68
81
  ## macOS folder permissions
69
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/remcp",
3
- "version": "0.2.207",
3
+ "version": "0.2.209",
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",
@@ -2,7 +2,6 @@
2
2
  // the server uses to recognise this computer across pairings.
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import process from 'node:process';
6
5
  import { randomUUID } from 'node:crypto';
7
6
  import { normalizeRuntime } from '../runtime.mjs';
8
7
 
@@ -79,13 +78,12 @@ export function writeJsonFile(file, value) {
79
78
  fs.chmodSync(file, 0o600);
80
79
  }
81
80
 
82
- export function autoUpdateEnabled(config, platform = process.platform) {
83
- // Linux often runs unrelated applications through the same global npm prefix. Never
84
- // mutate that prefix implicitly: require `remcp auto-update on` for Linux agents.
85
- // Keep the historical preference for existing explicit opt-ins and other platforms.
86
- if (config?.autoUpdate === true) return true;
87
- if (config?.autoUpdate === false) return false;
88
- return platform !== 'linux';
81
+ export function autoUpdateEnabled(config) {
82
+ // This preference controls the two first-party ReMCP npm packages only. It never
83
+ // upgrades the Node.js interpreter or npm itself. All supported OSes keep the
84
+ // historical default of automatic ReMCP releases unless the owner opts out.
85
+ // Updates use the verified managed service prefix, not a shared global prefix.
86
+ return config?.autoUpdate !== false;
89
87
  }
90
88
 
91
89
  export function flagEnabled(value) {
@@ -62,7 +62,7 @@ function writeLinuxServiceLauncher(cliPath = globalCliPath(), nodePath = process
62
62
 
63
63
  export function installLinuxService(cliPath = globalCliPath(), nodePath = process.execPath) {
64
64
  const launcherFile = writeLinuxServiceLauncher(cliPath, nodePath);
65
- const unit = `[Unit]\nDescription=ReMCP device agent\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${quoteSystemd(launcherFile)} start --service\nRestart=always\nRestartSec=3\nNoNewPrivileges=true\n\n[Install]\nWantedBy=default.target\n`;
65
+ const unit = `[Unit]\nDescription=ReMCP device agent\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${quoteSystemd(launcherFile)} start --service\nRestart=always\nRestartSec=3\n\n[Install]\nWantedBy=default.target\n`;
66
66
  fs.mkdirSync(path.dirname(linuxServiceFile), { recursive: true });
67
67
  fs.writeFileSync(linuxServiceFile, unit);
68
68
  run('systemctl', ['--user', 'daemon-reload']);
@@ -279,23 +279,29 @@ export function ensureServiceIfRecorded(config) {
279
279
  const launcherFile = writeLinuxServiceLauncher(cliPath, nodePath);
280
280
  const unit = fs.readFileSync(fd, 'utf8');
281
281
  const expected = `ExecStart=${quoteSystemd(launcherFile)} start --service`;
282
- if (!unit.includes(expected)) {
283
- const repaired = /^ExecStart=/m.test(unit) ? unit.replace(/^ExecStart=.*$/m, expected) : '';
284
- if (!repaired) {
285
- reinstall = true;
286
- } else {
287
- const data = Buffer.from(repaired, 'utf8');
288
- fs.ftruncateSync(fd, 0);
289
- let offset = 0;
290
- while (offset < data.length) {
291
- const written = fs.writeSync(fd, data, offset, data.length - offset, offset);
292
- if (!written) throw new Error('Could not finish rewriting the systemd unit');
293
- offset += written;
294
- }
295
- fs.ftruncateSync(fd, data.length);
296
- fs.fsyncSync(fd);
297
- run('systemctl', ['--user', 'daemon-reload']);
282
+ // Legacy versions added NoNewPrivileges=true to the user agent unit.
283
+ // Drop that line when repairing so ordinary OS-authenticated sudo/polkit
284
+ // can work for the owner's session. ReMCP never becomes root by itself.
285
+ // Do not modify unrelated systemd services or owner-managed drop-ins.
286
+ let repaired = unit.replace(/^NoNewPrivileges=true\r?\n/gm, '');
287
+ if (!repaired.includes(expected)) {
288
+ repaired = /^ExecStart=/m.test(repaired)
289
+ ? repaired.replace(/^ExecStart=.*$/m, expected) : '';
290
+ }
291
+ if (!repaired) {
292
+ reinstall = true;
293
+ } else if (repaired !== unit) {
294
+ const data = Buffer.from(repaired, 'utf8');
295
+ fs.ftruncateSync(fd, 0);
296
+ let offset = 0;
297
+ while (offset < data.length) {
298
+ const written = fs.writeSync(fd, data, offset, data.length - offset, offset);
299
+ if (!written) throw new Error('Could not finish rewriting the systemd unit');
300
+ offset += written;
298
301
  }
302
+ fs.ftruncateSync(fd, data.length);
303
+ fs.fsyncSync(fd);
304
+ run('systemctl', ['--user', 'daemon-reload']);
299
305
  }
300
306
  } finally {
301
307
  fs.closeSync(fd);
package/src/cli.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import fs from 'node:fs';
1
2
  import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import process from 'node:process';
@@ -36,7 +37,20 @@ function parse(argv) {
36
37
  }
37
38
 
38
39
  function printHelp() {
39
- console.log(`ReMCP ${VERSION}\n\nCommands:\n remcp connect [--no-qr]\n remcp start\n remcp status\n remcp doctor\n remcp update\n remcp install\n remcp uninstall\n remcp uninstall --purge\n remcp telemetry [status|on|off]\n remcp godmode [status|on|off]\n remcp --version\n\nPairing commands are generated in the ReMCP workspace.\n\nUsage metrics are opt-out (tool names, timings, outcomes only, sent to your own ReMCP\naccount through the paired agent). Disable them at any time with: remcp telemetry off\n\nUnrestricted mode (remcp godmode on) lifts the access roots and the command guardrails for this\ncomputer only. The MCP configuration tool cannot enable it; protect remote terminal access.`);
40
+ console.log(`ReMCP ${VERSION}\n\nCommands:\n remcp connect [--no-qr]\n remcp start\n remcp status\n remcp doctor\n remcp update\n remcp auto-update [status|on|off]\n remcp install\n remcp uninstall\n remcp uninstall --purge\n remcp telemetry [status|on|off]\n remcp godmode [status|on|off]\n remcp --version\n\nPairing commands are generated in the ReMCP workspace.\n\nUsage metrics are opt-out (tool names, timings, outcomes only, sent to your own ReMCP\naccount through the paired agent). Disable them at any time with: remcp telemetry off\n\nUnrestricted mode (remcp godmode on) lifts the access roots and the command guardrails for this\ncomputer only. The MCP configuration tool cannot enable it; protect remote terminal access.`);
41
+ }
42
+
43
+ // The running agent snapshots update policy at startup. A saved toggle alone does not
44
+ // change its six-hour polling loop, so reload a managed service after a policy change.
45
+ // Defer Linux handoff to avoid terminating a remote command before it returns.
46
+ export function applyAutoUpdatePreference(cfg, enabled, {
47
+ persist = saveConfig,
48
+ restart = restartPersistentServiceIfInstalled,
49
+ } = {}) {
50
+ const changed = autoUpdateEnabled(cfg) !== enabled;
51
+ const updated = { ...cfg, autoUpdate: enabled };
52
+ persist(updated);
53
+ return changed ? restart(updated, { deferLinux:true }) : null;
40
54
  }
41
55
 
42
56
  export async function main(argv = process.argv.slice(2)) {
@@ -201,8 +215,15 @@ export async function main(argv = process.argv.slice(2)) {
201
215
  console.log(JSON.stringify({ autoUpdate: autoUpdateEnabled(cfg), checkIntervalHours: 6 }, null, 2));
202
216
  return;
203
217
  }
204
- if (action === 'on' || action === 'enable') { saveConfig({ ...cfg, autoUpdate: true }); console.log('Auto-update enabled.'); return; }
205
- if (action === 'off' || action === 'disable') { saveConfig({ ...cfg, autoUpdate: false }); console.log('Auto-update disabled. Run `remcp update` yourself when you want a new version.'); return; }
218
+ if (['on', 'enable', 'off', 'disable'].includes(action)) {
219
+ const enabled = action === 'on' || action === 'enable';
220
+ const changed = autoUpdateEnabled(cfg) !== enabled;
221
+ const restarted = applyAutoUpdatePreference(cfg, enabled);
222
+ console.log(enabled ? 'Auto-update enabled.' : 'Auto-update disabled. Run `remcp update` yourself when you want a new version.');
223
+ if (restarted) console.log(`ReMCP agent ${restarted} to apply the update policy.`);
224
+ else if (changed) console.log('Restart any manually managed ReMCP agent to apply the update policy.');
225
+ return;
226
+ }
206
227
  throw new Error('Usage: remcp auto-update [status|on|off]');
207
228
  }
208
229
 
@@ -284,11 +305,15 @@ export async function main(argv = process.argv.slice(2)) {
284
305
  const targetClient = String(advertised?.cliVersion || '').trim() || null;
285
306
  const targetRuntime = String(advertised?.runtime || '').trim() || null;
286
307
  const configuredRuntime = String(cfg.runtime.packageSpec || '');
308
+ // The history also records CLI paths from old Node.js environments. An entry for
309
+ // a no-longer-existing path is not an additional installed package or agent.
287
310
  const installationPaths = new Set([
288
311
  currentInstall.cliPath,
289
312
  serviceInstall.cliPath,
290
313
  ...(Array.isArray(cfg.installations) ? cfg.installations.map(item => item?.cliPath) : []),
291
- ].filter(Boolean));
314
+ ].filter(file => typeof file === 'string' && fs.existsSync(file)).map(file => {
315
+ try { return fs.realpathSync(file); } catch { return file; }
316
+ }));
292
317
  const multipleInstallations = installationPaths.size > 1;
293
318
  const knownCliVersions = [VERSION, serviceInstall.cliVersion].filter(Boolean);
294
319
  const knownRuntimeVersions = [installedRuntime, serviceInstall.runtimeVersion].filter(Boolean);