@remcp/remcp 0.2.237 → 0.2.238

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.237",
3
+ "version": "0.2.238",
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-rollout.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/supervised-agent-lifecycle.mjs && node --check src/cli/update.mjs && node --check src/cli/windows-cli-shim.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",
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-rollout.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-linux.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/supervised-agent-lifecycle.mjs && node --check src/cli/update.mjs && node --check src/cli/windows-cli-shim.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": {
@@ -5,6 +5,7 @@ import { spawnSync } from 'node:child_process';
5
5
  import { PACKAGE_NAME, VERSION } from '../version.mjs';
6
6
  import { saveConfig } from './config.mjs';
7
7
  import { linuxServiceFile, linuxServiceLauncherFile, macServiceFile, macServiceLabel, windowsStartupScriptFile, windowsTaskName } from './env.mjs';
8
+ import { linuxSystemAgentOwned } from './service-linux.mjs';
8
9
  import { servicePlatform, globalCliPath, npmGlobalUpdateForNode } from './npm-global.mjs';
9
10
  import { windowsStartupScriptInstalled } from './windows-supervisor.mjs';
10
11
 
@@ -12,7 +13,7 @@ export function persistentServiceExpected(config) {
12
13
  if (config?.serviceInstalled === false) return false;
13
14
  if (config?.serviceInstalled === true) return true;
14
15
  const platform = servicePlatform();
15
- if (platform === 'linux') return fs.existsSync(linuxServiceFile);
16
+ if (platform === 'linux') return linuxSystemAgentOwned() || fs.existsSync(linuxServiceFile);
16
17
  if (platform === 'darwin') return fs.existsSync(macServiceFile);
17
18
  if (platform === 'win32') {
18
19
  return windowsStartupScriptInstalled(windowsStartupScriptFile) ||
@@ -25,9 +26,10 @@ export function persistentServiceState(config) {
25
26
  const platform = servicePlatform();
26
27
  const expected = persistentServiceExpected(config);
27
28
  if (platform === 'linux') {
28
- const installed = fs.existsSync(linuxServiceFile);
29
- const active = installed && spawnSync('systemctl', ['--user', 'is-active', '--quiet', 'remcp-agent.service'], { stdio: 'ignore' }).status === 0;
30
- return { expected, installed, active, manager:'systemd-user', name:'remcp-agent.service' };
29
+ const system = linuxSystemAgentOwned();
30
+ const installed = system || fs.existsSync(linuxServiceFile);
31
+ const active = installed && spawnSync('systemctl', [...(system ? [] : ['--user']), 'is-active', '--quiet', 'remcp-agent.service'], { stdio: 'ignore' }).status === 0;
32
+ return { expected, installed, active, manager:system ? 'systemd-system' : 'systemd-user', name:'remcp-agent.service' };
31
33
  }
32
34
  if (platform === 'darwin') {
33
35
  const installed = fs.existsSync(macServiceFile);
@@ -0,0 +1,92 @@
1
+ // Linux headless agents may be supervised by PID 1. Never mistake a system unit for
2
+ // a session-owned user unit during status checks, repairs or auto-updates.
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+ import { spawnSync } from 'node:child_process';
8
+ import { home, linuxServiceFile, linuxServiceLauncherFile } from './env.mjs';
9
+ import { resolvedCliScript, shellQuote } from './service-paths.mjs';
10
+
11
+ export const linuxSystemServiceFile = '/etc/systemd/system/remcp-agent.service';
12
+
13
+ export function isManagedLinuxSystemUnit(text, launcher = linuxServiceLauncherFile) {
14
+ if (typeof text !== 'string') return false;
15
+ const expected = new Set([
16
+ 'ExecStart="' + launcher.replaceAll('\\', '\\\\').replaceAll('"', '\\"') + '" start --service',
17
+ 'ExecStart=' + launcher + ' start --service',
18
+ ]);
19
+ return text.split(/\r?\n/).some(line => expected.has(line.trim()));
20
+ }
21
+
22
+ export function linuxSystemAgentOwned() {
23
+ if (process.platform !== 'linux' || process.getuid?.() !== 0) return false;
24
+ try {
25
+ return isManagedLinuxSystemUnit(fs.readFileSync(linuxSystemServiceFile, 'utf8'));
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ export function linuxServiceScope() {
32
+ return linuxSystemAgentOwned() ? 'system' : 'user';
33
+ }
34
+
35
+ export function linuxActiveServiceFile() {
36
+ return linuxServiceScope() === 'system' ? linuxSystemServiceFile : linuxServiceFile;
37
+ }
38
+
39
+ // Without linger, logind terminates user@UID.service at the final logout,
40
+ // even if systemctl --user enable --now reported success.
41
+ export function ensureLinuxLinger() {
42
+ if (process.platform !== 'linux') return;
43
+ // Tests isolate HOME in a temporary fixture. Never mutate the real host's loginctl
44
+ // configuration on behalf of that synthetic service installation.
45
+ if (path.resolve(home) !== path.resolve(os.userInfo().homedir)) {
46
+ if (process.env.NODE_ENV === 'test') return;
47
+ throw new Error('Refusing a persistent Linux service with HOME different from the account home directory.');
48
+ }
49
+ const username = os.userInfo().username;
50
+ const query = () => spawnSync('loginctl', ['show-user', username, '-p', 'Linger', '--value'],
51
+ { encoding:'utf8', timeout:10000 });
52
+ let state = query();
53
+ if (state.status === 0 && state.stdout.trim() === 'yes') return;
54
+ const enabled = spawnSync('loginctl', ['enable-linger', username],
55
+ { encoding:'utf8', timeout:10000 });
56
+ if (enabled.status !== 0) {
57
+ throw new Error('ReMCP cannot guarantee background operation after logout: '
58
+ + 'loginctl enable-linger failed. Ask an administrator to enable lingering for this account.');
59
+ }
60
+ state = query();
61
+ if (state.status !== 0 || state.stdout.trim() !== 'yes') {
62
+ throw new Error('ReMCP cannot verify systemd lingering; refusing to claim persistent installation.');
63
+ }
64
+ }
65
+
66
+ // The vetted bootstrap may install npm under ~/.local/share/remcp/npm and Node
67
+ // under ~/.local/share/remcp/bootstrap; neither bin is necessarily on login PATH.
68
+ export function linuxCliShimText(cliPath, nodePath) {
69
+ const entry = resolvedCliScript(cliPath);
70
+ const suffix = path.join('lib', 'node_modules', '@remcp', 'remcp', 'bin', 'remcp.mjs');
71
+ const prefix = entry.endsWith(path.sep + suffix)
72
+ ? entry.slice(0, -(suffix.length + 1)) : path.dirname(path.dirname(cliPath));
73
+ return '#!/bin/sh\n# ReMCP managed CLI shim\nNPM_CONFIG_PREFIX=' + shellQuote(prefix)
74
+ + '\nexport NPM_CONFIG_PREFIX\nPATH=' + shellQuote(path.dirname(nodePath)) + ':$PATH'
75
+ + '\nexport PATH\nexec ' + shellQuote(nodePath) + ' ' + shellQuote(entry) + ' "$@"\n';
76
+ }
77
+
78
+ export function ensureLinuxCliCommand(cliPath, nodePath) {
79
+ if (process.platform !== 'linux') return;
80
+ const destination = process.getuid?.() === 0
81
+ ? '/usr/local/bin/remcp' : path.join(home, '.local', 'bin', 'remcp');
82
+ fs.mkdirSync(path.dirname(destination), { recursive:true, mode:0o755 });
83
+ const marker = '# ReMCP managed CLI shim';
84
+ if (fs.existsSync(destination)) {
85
+ const current = fs.readFileSync(destination, 'utf8');
86
+ if (!current.includes(marker)) return; // Never overwrite an unrelated executable.
87
+ }
88
+ const content = linuxCliShimText(cliPath, nodePath);
89
+ const temp = destination + '.' + process.pid + '.tmp';
90
+ fs.writeFileSync(temp, content, { mode:0o755 });
91
+ fs.renameSync(temp, destination);
92
+ }
@@ -5,6 +5,7 @@ export { persistentServiceExpected, persistentServiceState, releasePairPresentAt
5
5
  // Installing, repairing and removing the background agent: one supervisor per platform, plus the
6
6
  // write-access tweaks a fresh install needs. Service maintenance follows local OS permissions.
7
7
  import fs from 'node:fs';
8
+ import os from 'node:os';
8
9
  import path from 'node:path';
9
10
  import process from 'node:process';
10
11
  import { spawnSync } from 'node:child_process';
@@ -17,6 +18,7 @@ import { PACKAGE_NAME, VERSION } from '../version.mjs';
17
18
  import { saveConfig } from './config.mjs';
18
19
  import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macServiceFile, macServiceLabel, windowsServiceLauncherFile, windowsStartupScriptFile, windowsTaskName } from './env.mjs';
19
20
  import { run } from './shell.mjs';
21
+ import { linuxActiveServiceFile, linuxServiceScope, linuxSystemAgentOwned, linuxSystemServiceFile, ensureLinuxLinger, ensureLinuxCliCommand } from './service-linux.mjs';
20
22
  import { windowsServiceLauncherText, windowsTaskAbsenceCommand, windowsStartupScriptText, windowsStartupScriptInstalled } from './windows-supervisor.mjs';
21
23
  import { ensureWindowsSupervisorKey, windowsSupervisorCommand } from './windows-user-supervisor.mjs';
22
24
 
@@ -62,11 +64,30 @@ function writeLinuxServiceLauncher(cliPath = globalCliPath(), nodePath = process
62
64
 
63
65
  export function installLinuxService(cliPath = globalCliPath(), nodePath = process.execPath) {
64
66
  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\n\n[Install]\nWantedBy=default.target\n`;
66
- fs.mkdirSync(path.dirname(linuxServiceFile), { recursive: true });
67
- fs.writeFileSync(linuxServiceFile, unit);
68
- run('systemctl', ['--user', 'daemon-reload']);
69
- run('systemctl', ['--user', 'enable', '--now', 'remcp-agent.service']);
67
+ // Fresh root installs are boot services owned by PID 1. Legacy per-user units are
68
+ // left under their existing supervisor, but must have linger enabled.
69
+ const privileged = process.getuid?.() === 0 && path.resolve(home) === path.resolve(os.userInfo().homedir);
70
+ if (privileged && fs.existsSync(linuxSystemServiceFile) && !linuxSystemAgentOwned()) {
71
+ throw new Error('Refusing to replace an unmanaged systemd remcp-agent.service');
72
+ }
73
+ const system = privileged && (linuxSystemAgentOwned() || !fs.existsSync(linuxServiceFile));
74
+ if (system) {
75
+ if (fs.existsSync(linuxSystemServiceFile) && !linuxSystemAgentOwned()) {
76
+ throw new Error('Refusing to replace an unmanaged systemd remcp-agent.service');
77
+ }
78
+ const unit = `[Unit]\nDescription=ReMCP headless device agent (system service)\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nUser=root\nWorkingDirectory=/root\nEnvironment=HOME=/root\nUMask=0077\nExecStart=${quoteSystemd(launcherFile)} start --service\nRestart=always\nRestartSec=5\n\n[Install]\nWantedBy=multi-user.target\n`;
79
+ fs.writeFileSync(linuxSystemServiceFile, unit, { mode:0o644 });
80
+ run('systemctl', ['daemon-reload']);
81
+ run('systemctl', ['enable', '--now', 'remcp-agent.service']);
82
+ } else {
83
+ ensureLinuxLinger();
84
+ 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`;
85
+ fs.mkdirSync(path.dirname(linuxServiceFile), { recursive: true });
86
+ fs.writeFileSync(linuxServiceFile, unit);
87
+ run('systemctl', ['--user', 'daemon-reload']);
88
+ run('systemctl', ['--user', 'enable', '--now', 'remcp-agent.service']);
89
+ }
90
+ ensureLinuxCliCommand(cliPath, nodePath);
70
91
  }
71
92
 
72
93
  export function writeWindowsServiceLauncher(cliPath = globalCliPath(), nodePath = process.execPath) {
@@ -241,11 +262,13 @@ export function ensureServiceIfRecorded(config) {
241
262
  const platform = servicePlatform();
242
263
  let restartScheduled = null;
243
264
  if (platform === 'linux') {
244
- // Read and repair the unit through one descriptor. This avoids a check/read/write race where
245
- // the path could be replaced between validation and mutation.
265
+ const scope = linuxServiceScope();
266
+ if (scope === 'user') ensureLinuxLinger();
267
+ // Read and repair the active unit through one descriptor. Never create a
268
+ // second user agent when a PID-1 system service already owns this pairing.
246
269
  let fd = null;
247
270
  try {
248
- fd = fs.openSync(linuxServiceFile, fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW || 0));
271
+ fd = fs.openSync(linuxActiveServiceFile(), fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW || 0));
249
272
  } catch (error) {
250
273
  if (error?.code !== 'ENOENT') throw error;
251
274
  }
@@ -279,7 +302,7 @@ export function ensureServiceIfRecorded(config) {
279
302
  }
280
303
  fs.ftruncateSync(fd, data.length);
281
304
  fs.fsyncSync(fd);
282
- run('systemctl', ['--user', 'daemon-reload']);
305
+ run('systemctl', [...(scope === 'user' ? ['--user'] : []), 'daemon-reload']);
283
306
  }
284
307
  } finally {
285
308
  fs.closeSync(fd);
@@ -308,24 +331,28 @@ export function ensureServiceIfRecorded(config) {
308
331
  }
309
332
  }
310
333
 
311
- export function deferredLinuxAgentRestartArgs(pid) {
334
+ export function deferredLinuxAgentRestartArgs(pid, scope = 'user') {
312
335
  if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('Invalid process identifier');
313
- return ['--user', '--on-active=3s', `--unit=remcp-agent-settings-${pid}`,
314
- 'systemctl', '--user', 'restart', 'remcp-agent.service'];
336
+ if (scope !== 'user' && scope !== 'system') throw new Error('Invalid Linux service scope');
337
+ const option = scope === 'user' ? ['--user'] : [];
338
+ return [...option, '--on-active=3s', `--unit=remcp-agent-settings-${pid}`,
339
+ 'systemctl', ...option, 'restart', 'remcp-agent.service'];
315
340
  }
316
341
 
317
342
  export function restartPersistentServiceIfInstalled(config, { deferLinux = false, deferWindows = false } = {}) {
318
343
  if (!persistentServiceExpected(config)) return null;
319
344
  const platform = servicePlatform();
320
- if (platform === 'linux' && fs.existsSync(linuxServiceFile)) {
345
+ if (platform === 'linux' && fs.existsSync(linuxActiveServiceFile())) {
346
+ const scope = linuxServiceScope();
347
+ const option = scope === 'user' ? ['--user'] : [];
321
348
  // Only owner-confirmed remote policy changes defer their restart. Ordinary
322
349
  // installs and updates preserve their existing synchronous supervisor contract.
323
350
  if (deferLinux) {
324
- run('systemd-run', deferredLinuxAgentRestartArgs(process.pid));
351
+ run('systemd-run', deferredLinuxAgentRestartArgs(process.pid, scope));
325
352
  return 'remcp-agent.service (restart scheduled)';
326
353
  }
327
- run('systemctl', ['--user', 'daemon-reload']);
328
- run('systemctl', ['--user', 'restart', 'remcp-agent.service']);
354
+ run('systemctl', [...option, 'daemon-reload']);
355
+ run('systemctl', [...option, 'restart', 'remcp-agent.service']);
329
356
  return 'remcp-agent.service';
330
357
  }
331
358
  if (platform === 'darwin' && fs.existsSync(macServiceFile)) {
@@ -357,9 +384,11 @@ export function restartPersistentServiceIfInstalled(config, { deferLinux = false
357
384
  export function uninstallPersistentService() {
358
385
  const platform = servicePlatform();
359
386
  if (platform === 'linux') {
360
- spawnSync('systemctl', ['--user', 'disable', '--now', 'remcp-agent.service'], { stdio: 'inherit' });
361
- try { fs.unlinkSync(linuxServiceFile); } catch {}
362
- spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'inherit' });
387
+ const scope = linuxServiceScope();
388
+ const option = scope === 'user' ? ['--user'] : [];
389
+ spawnSync('systemctl', [...option, 'disable', '--now', 'remcp-agent.service'], { stdio:'inherit' });
390
+ try { fs.unlinkSync(linuxActiveServiceFile()); } catch {}
391
+ spawnSync('systemctl', [...option, 'daemon-reload'], { stdio:'inherit' });
363
392
  } else if (platform === 'darwin') {
364
393
  const domain = macLaunchDomain();
365
394
  spawnSync('launchctl', ['bootout', domain, macServiceFile], { stdio: 'ignore' });
@@ -12,6 +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 { linuxSystemAgentOwned } from './service-linux.mjs';
15
16
  import { assertWindowsUpdateLifecycleSafe, currentInstallationInfo, ensureMacCliCommand, ensureServiceIfRecorded, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, servicePlatform, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
16
17
 
17
18
  const UPDATE_DISCOVERY_TIMEOUT_MS = 5000;
@@ -235,7 +236,7 @@ export async function updateCommand(flags) {
235
236
  updateSource: targets.source,
236
237
  installable: targets.installable,
237
238
  upToDate: alreadyCurrent,
238
- managedService: fs.existsSync(linuxServiceFile) || fs.existsSync(macServiceFile),
239
+ managedService: linuxSystemAgentOwned() || fs.existsSync(linuxServiceFile) || fs.existsSync(macServiceFile),
239
240
  supervisor: supervisorRestart() ?? 'none',
240
241
  }, null, 2));
241
242
  return;