@remcp/remcp 0.2.237 → 0.2.239

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.239",
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,95 @@
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
+ // Test-only synthetic HOME has no logind account and must never mutate the host.
44
+ // Guard before os.userInfo(): an isolated CI UID may not have a passwd entry.
45
+ if (process.env.NODE_ENV === 'test' && process.env.REMCP_TEST_PLATFORM === 'linux') return;
46
+ let account;
47
+ try { account = os.userInfo(); }
48
+ catch { throw new Error('ReMCP cannot resolve the current Linux account for persistent systemd supervision.'); }
49
+ if (path.resolve(home) !== path.resolve(account.homedir)) {
50
+ throw new Error('Refusing a persistent Linux service with HOME different from the account home directory.');
51
+ }
52
+ const username = account.username;
53
+ const query = () => spawnSync('loginctl', ['show-user', username, '-p', 'Linger', '--value'],
54
+ { encoding:'utf8', timeout:10000 });
55
+ let state = query();
56
+ if (state.status === 0 && state.stdout.trim() === 'yes') return;
57
+ const enabled = spawnSync('loginctl', ['enable-linger', username],
58
+ { encoding:'utf8', timeout:10000 });
59
+ if (enabled.status !== 0) {
60
+ throw new Error('ReMCP cannot guarantee background operation after logout: '
61
+ + 'loginctl enable-linger failed. Ask an administrator to enable lingering for this account.');
62
+ }
63
+ state = query();
64
+ if (state.status !== 0 || state.stdout.trim() !== 'yes') {
65
+ throw new Error('ReMCP cannot verify systemd lingering; refusing to claim persistent installation.');
66
+ }
67
+ }
68
+
69
+ // The vetted bootstrap may install npm under ~/.local/share/remcp/npm and Node
70
+ // under ~/.local/share/remcp/bootstrap; neither bin is necessarily on login PATH.
71
+ export function linuxCliShimText(cliPath, nodePath) {
72
+ const entry = resolvedCliScript(cliPath);
73
+ const suffix = path.join('lib', 'node_modules', '@remcp', 'remcp', 'bin', 'remcp.mjs');
74
+ const prefix = entry.endsWith(path.sep + suffix)
75
+ ? entry.slice(0, -(suffix.length + 1)) : path.dirname(path.dirname(cliPath));
76
+ return '#!/bin/sh\n# ReMCP managed CLI shim\nNPM_CONFIG_PREFIX=' + shellQuote(prefix)
77
+ + '\nexport NPM_CONFIG_PREFIX\nPATH=' + shellQuote(path.dirname(nodePath)) + ':$PATH'
78
+ + '\nexport PATH\nexec ' + shellQuote(nodePath) + ' ' + shellQuote(entry) + ' "$@"\n';
79
+ }
80
+
81
+ export function ensureLinuxCliCommand(cliPath, nodePath) {
82
+ if (process.platform !== 'linux') return;
83
+ const destination = process.getuid?.() === 0
84
+ ? '/usr/local/bin/remcp' : path.join(home, '.local', 'bin', 'remcp');
85
+ fs.mkdirSync(path.dirname(destination), { recursive:true, mode:0o755 });
86
+ const marker = '# ReMCP managed CLI shim';
87
+ if (fs.existsSync(destination)) {
88
+ const current = fs.readFileSync(destination, 'utf8');
89
+ if (!current.includes(marker)) return; // Never overwrite an unrelated executable.
90
+ }
91
+ const content = linuxCliShimText(cliPath, nodePath);
92
+ const temp = destination + '.' + process.pid + '.tmp';
93
+ fs.writeFileSync(temp, content, { mode:0o755 });
94
+ fs.renameSync(temp, destination);
95
+ }
@@ -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,32 @@ 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 syntheticLinuxFixture = process.env.NODE_ENV === 'test' && process.env.REMCP_TEST_PLATFORM === 'linux';
70
+ const privileged = !syntheticLinuxFixture && process.getuid?.() === 0
71
+ && path.resolve(home) === path.resolve(os.userInfo().homedir);
72
+ if (privileged && fs.existsSync(linuxSystemServiceFile) && !linuxSystemAgentOwned()) {
73
+ throw new Error('Refusing to replace an unmanaged systemd remcp-agent.service');
74
+ }
75
+ const system = privileged && (linuxSystemAgentOwned() || !fs.existsSync(linuxServiceFile));
76
+ if (system) {
77
+ if (fs.existsSync(linuxSystemServiceFile) && !linuxSystemAgentOwned()) {
78
+ throw new Error('Refusing to replace an unmanaged systemd remcp-agent.service');
79
+ }
80
+ 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`;
81
+ fs.writeFileSync(linuxSystemServiceFile, unit, { mode:0o644 });
82
+ run('systemctl', ['daemon-reload']);
83
+ run('systemctl', ['enable', '--now', 'remcp-agent.service']);
84
+ } else {
85
+ ensureLinuxLinger();
86
+ 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`;
87
+ fs.mkdirSync(path.dirname(linuxServiceFile), { recursive: true });
88
+ fs.writeFileSync(linuxServiceFile, unit);
89
+ run('systemctl', ['--user', 'daemon-reload']);
90
+ run('systemctl', ['--user', 'enable', '--now', 'remcp-agent.service']);
91
+ }
92
+ ensureLinuxCliCommand(cliPath, nodePath);
70
93
  }
71
94
 
72
95
  export function writeWindowsServiceLauncher(cliPath = globalCliPath(), nodePath = process.execPath) {
@@ -241,11 +264,13 @@ export function ensureServiceIfRecorded(config) {
241
264
  const platform = servicePlatform();
242
265
  let restartScheduled = null;
243
266
  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.
267
+ const scope = linuxServiceScope();
268
+ if (scope === 'user') ensureLinuxLinger();
269
+ // Read and repair the active unit through one descriptor. Never create a
270
+ // second user agent when a PID-1 system service already owns this pairing.
246
271
  let fd = null;
247
272
  try {
248
- fd = fs.openSync(linuxServiceFile, fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW || 0));
273
+ fd = fs.openSync(linuxActiveServiceFile(), fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW || 0));
249
274
  } catch (error) {
250
275
  if (error?.code !== 'ENOENT') throw error;
251
276
  }
@@ -279,7 +304,7 @@ export function ensureServiceIfRecorded(config) {
279
304
  }
280
305
  fs.ftruncateSync(fd, data.length);
281
306
  fs.fsyncSync(fd);
282
- run('systemctl', ['--user', 'daemon-reload']);
307
+ run('systemctl', [...(scope === 'user' ? ['--user'] : []), 'daemon-reload']);
283
308
  }
284
309
  } finally {
285
310
  fs.closeSync(fd);
@@ -308,24 +333,28 @@ export function ensureServiceIfRecorded(config) {
308
333
  }
309
334
  }
310
335
 
311
- export function deferredLinuxAgentRestartArgs(pid) {
336
+ export function deferredLinuxAgentRestartArgs(pid, scope = 'user') {
312
337
  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'];
338
+ if (scope !== 'user' && scope !== 'system') throw new Error('Invalid Linux service scope');
339
+ const option = scope === 'user' ? ['--user'] : [];
340
+ return [...option, '--on-active=3s', `--unit=remcp-agent-settings-${pid}`,
341
+ 'systemctl', ...option, 'restart', 'remcp-agent.service'];
315
342
  }
316
343
 
317
344
  export function restartPersistentServiceIfInstalled(config, { deferLinux = false, deferWindows = false } = {}) {
318
345
  if (!persistentServiceExpected(config)) return null;
319
346
  const platform = servicePlatform();
320
- if (platform === 'linux' && fs.existsSync(linuxServiceFile)) {
347
+ if (platform === 'linux' && fs.existsSync(linuxActiveServiceFile())) {
348
+ const scope = linuxServiceScope();
349
+ const option = scope === 'user' ? ['--user'] : [];
321
350
  // Only owner-confirmed remote policy changes defer their restart. Ordinary
322
351
  // installs and updates preserve their existing synchronous supervisor contract.
323
352
  if (deferLinux) {
324
- run('systemd-run', deferredLinuxAgentRestartArgs(process.pid));
353
+ run('systemd-run', deferredLinuxAgentRestartArgs(process.pid, scope));
325
354
  return 'remcp-agent.service (restart scheduled)';
326
355
  }
327
- run('systemctl', ['--user', 'daemon-reload']);
328
- run('systemctl', ['--user', 'restart', 'remcp-agent.service']);
356
+ run('systemctl', [...option, 'daemon-reload']);
357
+ run('systemctl', [...option, 'restart', 'remcp-agent.service']);
329
358
  return 'remcp-agent.service';
330
359
  }
331
360
  if (platform === 'darwin' && fs.existsSync(macServiceFile)) {
@@ -357,9 +386,11 @@ export function restartPersistentServiceIfInstalled(config, { deferLinux = false
357
386
  export function uninstallPersistentService() {
358
387
  const platform = servicePlatform();
359
388
  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' });
389
+ const scope = linuxServiceScope();
390
+ const option = scope === 'user' ? ['--user'] : [];
391
+ spawnSync('systemctl', [...option, 'disable', '--now', 'remcp-agent.service'], { stdio:'inherit' });
392
+ try { fs.unlinkSync(linuxActiveServiceFile()); } catch {}
393
+ spawnSync('systemctl', [...option, 'daemon-reload'], { stdio:'inherit' });
363
394
  } else if (platform === 'darwin') {
364
395
  const domain = macLaunchDomain();
365
396
  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;