machine-bridge-mcp 0.11.0 → 0.12.0
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 +39 -0
- package/CONTRIBUTING.md +2 -2
- package/README.md +6 -6
- package/SECURITY.md +15 -5
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +15 -7
- package/docs/AUDIT.md +110 -0
- package/docs/ENGINEERING.md +14 -4
- package/docs/LOCAL_AUTOMATION.md +3 -3
- package/docs/LOGGING.md +5 -3
- package/docs/MANAGED_JOBS.md +5 -1
- package/docs/OPERATIONS.md +15 -3
- package/docs/PRIVACY.md +5 -3
- package/docs/RELEASING.md +1 -1
- package/docs/TESTING.md +11 -8
- package/package.json +7 -8
- package/scripts/github-release.mjs +368 -0
- package/scripts/privacy-check.mjs +66 -5
- package/scripts/release-state.mjs +10 -0
- package/scripts/syntax-check.mjs +52 -0
- package/src/local/app-automation.mjs +3 -1
- package/src/local/browser-bridge.mjs +94 -53
- package/src/local/cli.mjs +218 -130
- package/src/local/daemon-process.mjs +199 -0
- package/src/local/exclusive-file.mjs +94 -0
- package/src/local/job-runner.mjs +33 -17
- package/src/local/log.mjs +16 -1
- package/src/local/managed-jobs.mjs +187 -56
- package/src/local/process-identity.mjs +143 -0
- package/src/local/resource-operations.mjs +2 -3
- package/src/local/runtime.mjs +4 -2
- package/src/local/service-lifecycle.mjs +56 -0
- package/src/local/service.mjs +115 -36
- package/src/local/shell.mjs +23 -4
- package/src/local/state.mjs +228 -66
- package/src/worker/index.ts +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export async function stopAndRemoveAutostart({
|
|
2
|
+
states = [],
|
|
3
|
+
stateRoot,
|
|
4
|
+
logger = console,
|
|
5
|
+
reason = "service uninstall",
|
|
6
|
+
stopAutostart,
|
|
7
|
+
uninstallAutostart,
|
|
8
|
+
stopWorkspaceServiceDaemon,
|
|
9
|
+
} = {}) {
|
|
10
|
+
assertFunction(stopAutostart, "stopAutostart");
|
|
11
|
+
assertFunction(uninstallAutostart, "uninstallAutostart");
|
|
12
|
+
assertFunction(stopWorkspaceServiceDaemon, "stopWorkspaceServiceDaemon");
|
|
13
|
+
|
|
14
|
+
const platformStop = await stopAutostart({ logger });
|
|
15
|
+
if (platformStop?.ok !== true) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
removed: false,
|
|
19
|
+
reason: "platform_stop_failed",
|
|
20
|
+
platform_stop: platformStop,
|
|
21
|
+
workspace_daemons: [],
|
|
22
|
+
removal: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const workspaceDaemons = [];
|
|
27
|
+
for (const state of states) {
|
|
28
|
+
const result = await stopWorkspaceServiceDaemon(state, { logger, reason });
|
|
29
|
+
workspaceDaemons.push({ workspace: state?.workspace?.path || null, ...result });
|
|
30
|
+
if (result.found && !result.ok) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
removed: false,
|
|
34
|
+
reason: "workspace_daemon_stop_failed",
|
|
35
|
+
platform_stop: platformStop,
|
|
36
|
+
workspace_daemons: workspaceDaemons,
|
|
37
|
+
removal: null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const removal = await uninstallAutostart({ stateRoot, logger });
|
|
43
|
+
const removed = removal?.ok === true;
|
|
44
|
+
return {
|
|
45
|
+
ok: removed,
|
|
46
|
+
removed,
|
|
47
|
+
reason: removed ? "removed" : "platform_remove_failed",
|
|
48
|
+
platform_stop: platformStop,
|
|
49
|
+
workspace_daemons: workspaceDaemons,
|
|
50
|
+
removal,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertFunction(value, name) {
|
|
55
|
+
if (typeof value !== "function") throw new Error(`${name} is required`);
|
|
56
|
+
}
|
package/src/local/service.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, rmSync, statSync,
|
|
1
|
+
import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, rmSync, statSync, writeSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { run } from "./shell.mjs";
|
|
5
5
|
import { ensureOwnerOnlyDir, expandHome, ownerOnlyFile } from "./state.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
7
7
|
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
8
8
|
|
|
9
9
|
const LABEL = "dev.machine-bridge-mcp.daemon";
|
|
@@ -40,17 +40,34 @@ export async function autostartStatus({ logger = console } = {}) {
|
|
|
40
40
|
|
|
41
41
|
export async function startAutostart({ logger = console } = {}) {
|
|
42
42
|
if (process.platform === "darwin") return startLaunchd(logger);
|
|
43
|
-
if (process.platform === "win32")
|
|
44
|
-
|
|
43
|
+
if (process.platform === "win32") {
|
|
44
|
+
return normalizeServiceCommandResult("schtasks", await serviceRun("schtasks", ["/Run", "/TN", WINDOWS_TASK]));
|
|
45
|
+
}
|
|
46
|
+
return normalizeServiceCommandResult("systemd", await serviceRun("systemctl", ["--user", "start", "machine-bridge-mcp.service"]));
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export async function stopAutostart({ logger = console } = {}) {
|
|
48
50
|
if (process.platform === "darwin") return stopLaunchd(logger);
|
|
49
|
-
if (process.platform === "win32")
|
|
50
|
-
|
|
51
|
+
if (process.platform === "win32") {
|
|
52
|
+
return normalizeServiceCommandResult("schtasks", await serviceRun("schtasks", ["/End", "/TN", WINDOWS_TASK]), { allowAlreadyStopped: true });
|
|
53
|
+
}
|
|
54
|
+
return normalizeServiceCommandResult("systemd", await serviceRun("systemctl", ["--user", "stop", "machine-bridge-mcp.service"]), { allowAlreadyStopped: true });
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
|
|
58
|
+
|
|
59
|
+
export function normalizeServiceCommandResult(provider, result, { allowAlreadyStopped = false } = {}) {
|
|
60
|
+
const detail = `${result?.stdout || ""}
|
|
61
|
+
${result?.stderr || ""}`;
|
|
62
|
+
const alreadyStopped = allowAlreadyStopped && /(?:not loaded|not found|does not exist|cannot find|not running|inactive)/i.test(detail);
|
|
63
|
+
return {
|
|
64
|
+
...result,
|
|
65
|
+
ok: result?.code === 0 || alreadyStopped,
|
|
66
|
+
provider,
|
|
67
|
+
already_stopped: alreadyStopped,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
export function trimAutostartLogs(stateRoot, options = {}) {
|
|
55
72
|
const root = expandHome(stateRoot);
|
|
56
73
|
const maxBytes = Number.isFinite(Number(options.maxBytes)) ? Math.max(1024, Number(options.maxBytes)) : 2 * 1024 * 1024;
|
|
@@ -233,21 +250,26 @@ function writePrivateServiceFile(file, content) {
|
|
|
233
250
|
const info = lstatSync(file);
|
|
234
251
|
if (info.isSymbolicLink() || !info.isFile()) throw new Error("autostart configuration path must be a regular non-symbolic-link file");
|
|
235
252
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
writeFileSync(temporary, content, { mode: 0o600, flag: "wx" });
|
|
239
|
-
replaceFileSync(temporary, file);
|
|
240
|
-
ownerOnlyFile(file);
|
|
241
|
-
} catch (error) {
|
|
242
|
-
try { rmSync(temporary, { force: true }); } catch {}
|
|
243
|
-
throw error;
|
|
244
|
-
}
|
|
253
|
+
replaceFileAtomicallySync(file, content, { mode: 0o600 });
|
|
254
|
+
ownerOnlyFile(file);
|
|
245
255
|
}
|
|
246
256
|
|
|
247
257
|
function launchdPlistPath() {
|
|
248
258
|
return path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
249
259
|
}
|
|
250
260
|
|
|
261
|
+
export function launchdServiceTarget(uid = process.getuid?.()) {
|
|
262
|
+
const parsed = Number(uid);
|
|
263
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new Error("launchd requires a numeric user id");
|
|
264
|
+
return `gui/${parsed}/${LABEL}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function launchdDomainTarget(uid = process.getuid?.()) {
|
|
268
|
+
const parsed = Number(uid);
|
|
269
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new Error("launchd requires a numeric user id");
|
|
270
|
+
return `gui/${parsed}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
251
273
|
async function installLaunchd(spec, logger) {
|
|
252
274
|
const plistPath = launchdPlistPath();
|
|
253
275
|
mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
@@ -259,34 +281,73 @@ async function installLaunchd(spec, logger) {
|
|
|
259
281
|
|
|
260
282
|
async function startLaunchd(logger) {
|
|
261
283
|
const plistPath = launchdPlistPath();
|
|
262
|
-
const target =
|
|
284
|
+
const target = launchdDomainTarget();
|
|
263
285
|
if (!existsSync(plistPath)) return { ok: false, error: "launchd plist not installed" };
|
|
264
|
-
await
|
|
286
|
+
const stopped = await stopLaunchd({ info() {}, warn() {} });
|
|
287
|
+
if (!stopped.ok) return { ok: false, provider: "launchd", stop: stopped };
|
|
265
288
|
const boot = await serviceRun("launchctl", ["bootstrap", target, plistPath]);
|
|
266
289
|
const kick = await serviceRun("launchctl", ["kickstart", "-k", `${target}/${LABEL}`]);
|
|
267
|
-
|
|
268
|
-
|
|
290
|
+
const ok = boot.code === 0 || kick.code === 0;
|
|
291
|
+
if (ok) logger.info?.("launchd service started");
|
|
292
|
+
else logger.warn?.("launchd service failed to start");
|
|
293
|
+
return { ok, provider: "launchd", bootstrap: boot, kickstart: kick };
|
|
269
294
|
}
|
|
270
295
|
|
|
271
296
|
async function stopLaunchd(logger) {
|
|
272
297
|
const plistPath = launchdPlistPath();
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
298
|
+
const domainTarget = launchdDomainTarget();
|
|
299
|
+
const serviceTarget = launchdServiceTarget();
|
|
300
|
+
const before = await statusLaunchd();
|
|
301
|
+
if (!before.active) {
|
|
302
|
+
logger.info?.("launchd service is not loaded");
|
|
303
|
+
return {
|
|
304
|
+
ok: true,
|
|
305
|
+
provider: "launchd",
|
|
306
|
+
installed: existsSync(plistPath),
|
|
307
|
+
active_before: false,
|
|
308
|
+
active: false,
|
|
309
|
+
already_stopped: true,
|
|
310
|
+
code: 0,
|
|
311
|
+
stdout: "",
|
|
312
|
+
stderr: "",
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const byServiceTarget = await serviceRun("launchctl", ["bootout", serviceTarget]);
|
|
317
|
+
const byPlist = byServiceTarget.code === 0
|
|
318
|
+
? null
|
|
319
|
+
: await serviceRun("launchctl", ["bootout", domainTarget, plistPath]);
|
|
320
|
+
const after = await statusLaunchd();
|
|
321
|
+
const rawResult = byPlist || byServiceTarget;
|
|
322
|
+
const ok = !after.active;
|
|
323
|
+
if (ok) logger.info?.("launchd service stopped");
|
|
324
|
+
else logger.warn?.("launchd service is still active after the stop request");
|
|
325
|
+
return {
|
|
326
|
+
...rawResult,
|
|
327
|
+
ok,
|
|
328
|
+
provider: "launchd",
|
|
329
|
+
installed: existsSync(plistPath),
|
|
330
|
+
active_before: true,
|
|
331
|
+
active: after.active,
|
|
332
|
+
already_stopped: false,
|
|
333
|
+
code: ok ? 0 : rawResult.code,
|
|
334
|
+
bootout_service_target: byServiceTarget,
|
|
335
|
+
...(byPlist ? { bootout_plist_fallback: byPlist } : {}),
|
|
336
|
+
};
|
|
277
337
|
}
|
|
278
338
|
|
|
279
339
|
async function uninstallLaunchd(logger) {
|
|
280
|
-
await stopLaunchd(logger)
|
|
340
|
+
const stopped = await stopLaunchd(logger);
|
|
281
341
|
const plistPath = launchdPlistPath();
|
|
342
|
+
if (!stopped.ok) return { ok: false, provider: "launchd", path: plistPath, stop: stopped };
|
|
282
343
|
if (existsSync(plistPath)) rmSync(plistPath, { force: true });
|
|
283
344
|
logger.info?.("Autostart removed.");
|
|
284
|
-
return { ok: true, provider: "launchd", path: plistPath };
|
|
345
|
+
return { ok: true, provider: "launchd", path: plistPath, stop: stopped };
|
|
285
346
|
}
|
|
286
347
|
|
|
287
348
|
async function statusLaunchd() {
|
|
288
349
|
const plistPath = launchdPlistPath();
|
|
289
|
-
const target =
|
|
350
|
+
const target = launchdServiceTarget();
|
|
290
351
|
const result = await serviceRun("launchctl", ["print", target]);
|
|
291
352
|
return { ok: existsSync(plistPath), provider: "launchd", installed: existsSync(plistPath), path: plistPath, active: result.code === 0, detail: result.stdout || result.stderr };
|
|
292
353
|
}
|
|
@@ -328,17 +389,28 @@ async function installSystemd(spec, logger) {
|
|
|
328
389
|
const reload = await serviceRun("systemctl", ["--user", "daemon-reload"]);
|
|
329
390
|
const enable = await serviceRun("systemctl", ["--user", "enable", "machine-bridge-mcp.service"]);
|
|
330
391
|
const linger = await serviceRun("loginctl", ["enable-linger", os.userInfo().username]);
|
|
331
|
-
|
|
332
|
-
|
|
392
|
+
const ok = reload.code === 0 && enable.code === 0;
|
|
393
|
+
if (ok) logger.info?.("Autostart installed.");
|
|
394
|
+
else logger.warn?.("Autostart installation failed; the service definition was kept for diagnosis.");
|
|
395
|
+
return { ok, provider: "systemd", path: servicePath, reload, enable, linger };
|
|
333
396
|
}
|
|
334
397
|
|
|
335
398
|
async function uninstallSystemd(logger) {
|
|
336
|
-
await serviceRun("systemctl", ["--user", "disable", "--now", "machine-bridge-mcp.service"]);
|
|
337
399
|
const servicePath = systemdPath();
|
|
400
|
+
const disable = await serviceRun("systemctl", ["--user", "disable", "--now", "machine-bridge-mcp.service"]);
|
|
401
|
+
const activeCheck = await serviceRun("systemctl", ["--user", "is-active", "machine-bridge-mcp.service"]);
|
|
402
|
+
const active = activeCheck.code === 0;
|
|
403
|
+
if (active) {
|
|
404
|
+
logger.warn?.("systemd service is still active; its definition was not removed.");
|
|
405
|
+
return { ok: false, provider: "systemd", path: servicePath, disable, active_check: activeCheck, active: true };
|
|
406
|
+
}
|
|
338
407
|
if (existsSync(servicePath)) rmSync(servicePath, { force: true });
|
|
339
|
-
await serviceRun("systemctl", ["--user", "daemon-reload"]);
|
|
340
|
-
|
|
341
|
-
|
|
408
|
+
const reload = await serviceRun("systemctl", ["--user", "daemon-reload"]);
|
|
409
|
+
const ok = reload.code === 0 && (disable.code === 0 || /not loaded|not found|does not exist/i.test(`${disable.stdout}
|
|
410
|
+
${disable.stderr}`));
|
|
411
|
+
if (ok) logger.info?.("Autostart removed.");
|
|
412
|
+
else logger.warn?.("Autostart removal reported an error.");
|
|
413
|
+
return { ok, provider: "systemd", path: servicePath, disable, active_check: activeCheck, reload, active: false };
|
|
342
414
|
}
|
|
343
415
|
|
|
344
416
|
async function statusSystemd() {
|
|
@@ -370,14 +442,21 @@ WantedBy=default.target
|
|
|
370
442
|
async function installWindowsTask(spec, logger) {
|
|
371
443
|
const command = windowsCommand(spec);
|
|
372
444
|
const result = await serviceRun("schtasks", ["/Create", "/TN", WINDOWS_TASK, "/SC", "ONLOGON", "/TR", command, "/F"]);
|
|
373
|
-
|
|
374
|
-
|
|
445
|
+
const ok = result.code === 0;
|
|
446
|
+
if (ok) logger.info?.("Windows Scheduled Task installed for logon");
|
|
447
|
+
else logger.warn?.("Windows Scheduled Task installation failed");
|
|
448
|
+
return { ok, provider: "schtasks", task: WINDOWS_TASK, result };
|
|
375
449
|
}
|
|
376
450
|
|
|
377
451
|
async function uninstallWindowsTask(logger) {
|
|
452
|
+
const end = await serviceRun("schtasks", ["/End", "/TN", WINDOWS_TASK]);
|
|
378
453
|
const result = await serviceRun("schtasks", ["/Delete", "/TN", WINDOWS_TASK, "/F"]);
|
|
379
|
-
|
|
380
|
-
|
|
454
|
+
const missing = /cannot find|not exist|not found/i.test(`${result.stdout}
|
|
455
|
+
${result.stderr}`);
|
|
456
|
+
const ok = result.code === 0 || missing;
|
|
457
|
+
if (ok) logger.info?.("Windows Scheduled Task removed");
|
|
458
|
+
else logger.warn?.("Windows Scheduled Task removal failed");
|
|
459
|
+
return { ok, provider: "schtasks", task: WINDOWS_TASK, end, result };
|
|
381
460
|
}
|
|
382
461
|
|
|
383
462
|
async function statusWindowsTask() {
|
package/src/local/shell.mjs
CHANGED
|
@@ -12,6 +12,7 @@ export function run(command, args = [], options = {}) {
|
|
|
12
12
|
env: options.env || process.env,
|
|
13
13
|
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
14
14
|
shell: false,
|
|
15
|
+
detached: process.platform !== "win32",
|
|
15
16
|
windowsHide: true,
|
|
16
17
|
});
|
|
17
18
|
let stdout = "";
|
|
@@ -26,9 +27,8 @@ export function run(command, args = [], options = {}) {
|
|
|
26
27
|
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
27
28
|
timer = setTimeout(() => {
|
|
28
29
|
timedOut = true;
|
|
29
|
-
|
|
30
|
-
killTimer = setTimeout(() =>
|
|
31
|
-
killTimer.unref?.();
|
|
30
|
+
terminateCommandTree(child, false);
|
|
31
|
+
killTimer = setTimeout(() => terminateCommandTree(child, true), 2000);
|
|
32
32
|
}, timeoutMs);
|
|
33
33
|
timer.unref?.();
|
|
34
34
|
}
|
|
@@ -36,7 +36,7 @@ export function run(command, args = [], options = {}) {
|
|
|
36
36
|
if (settled) return;
|
|
37
37
|
settled = true;
|
|
38
38
|
if (timer) clearTimeout(timer);
|
|
39
|
-
if (killTimer) clearTimeout(killTimer);
|
|
39
|
+
if (killTimer && !timedOut) clearTimeout(killTimer);
|
|
40
40
|
callback();
|
|
41
41
|
};
|
|
42
42
|
if (capture) {
|
|
@@ -73,6 +73,25 @@ export function run(command, args = [], options = {}) {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
|
|
77
|
+
function terminateCommandTree(child, force) {
|
|
78
|
+
if (!child?.pid) return;
|
|
79
|
+
if (process.platform === "win32") {
|
|
80
|
+
try {
|
|
81
|
+
const killer = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", ...(force ? ["/F"] : [])], {
|
|
82
|
+
stdio: "ignore",
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
});
|
|
85
|
+
killer.unref();
|
|
86
|
+
return;
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
const signal = force ? "SIGKILL" : "SIGTERM";
|
|
90
|
+
try { process.kill(-child.pid, signal); } catch {
|
|
91
|
+
try { child.kill(signal); } catch {}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
function appendLimited(current, chunk, max) {
|
|
77
96
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? ""));
|
|
78
97
|
const budget = Math.max(0, max - Buffer.byteLength(current));
|