machine-bridge-mcp 2.0.0 → 3.0.0-beta.9
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 +101 -0
- package/CONTRIBUTING.md +20 -25
- package/README.md +21 -14
- package/SECURITY.md +147 -123
- package/browser-extension/manifest.json +3 -2
- package/browser-extension/service-worker.js +31 -12
- package/docs/ARCHITECTURE.md +27 -13
- package/docs/AUDIT.md +85 -2
- package/docs/CLIENTS.md +2 -2
- package/docs/ENGINEERING.md +9 -3
- package/docs/GETTING_STARTED.md +10 -9
- package/docs/LOCAL_AUTHORIZATION.md +115 -72
- package/docs/LOGGING.md +12 -3
- package/docs/MANAGED_JOBS.md +24 -29
- package/docs/MULTI_ACCOUNT.md +98 -37
- package/docs/OPERATIONS.md +33 -22
- package/docs/OVERVIEW.md +7 -7
- package/docs/PROJECT_STANDARDS.md +9 -5
- package/docs/RELEASING.md +171 -64
- package/docs/TESTING.md +19 -7
- package/docs/THREAT_MODEL.md +138 -67
- package/docs/TOOL_REFERENCE.md +2 -2
- package/docs/UPGRADING.md +102 -18
- package/native/macos/MachineBridgeTrustBroker.swift +233 -0
- package/package.json +36 -5
- package/scripts/candidate-runtime-store.mjs +38 -0
- package/scripts/check-plan.mjs +19 -0
- package/scripts/check-runner.mjs +4 -1
- package/scripts/commit-message-check.mjs +5 -2
- package/scripts/coverage-check.mjs +30 -2
- package/scripts/github-push.mjs +11 -0
- package/scripts/github-release.mjs +38 -7
- package/scripts/install-published-prerelease.mjs +167 -0
- package/scripts/local-release-acceptance.mjs +23 -12
- package/scripts/npm-publication-policy.mjs +16 -0
- package/scripts/prerelease-activation.mjs +79 -0
- package/scripts/privacy-check.mjs +7 -5
- package/scripts/promotion-digest.mjs +121 -0
- package/scripts/publish-npm.mjs +51 -0
- package/scripts/published-release.mjs +80 -0
- package/scripts/release-acceptance.mjs +30 -1
- package/scripts/release-candidate-manifest.mjs +40 -0
- package/scripts/release-channel.mjs +106 -0
- package/scripts/release-impact-check.mjs +12 -22
- package/scripts/release-soak.mjs +237 -0
- package/scripts/start-release-candidate.mjs +113 -7
- package/src/local/account-access.mjs +6 -0
- package/src/local/account-admin.mjs +34 -16
- package/src/local/agent-context.mjs +45 -42
- package/src/local/app-automation.mjs +9 -4
- package/src/local/authority-context.mjs +106 -0
- package/src/local/browser-bridge-http.mjs +4 -1
- package/src/local/browser-bridge.mjs +14 -9
- package/src/local/browser-broker-routes.mjs +4 -0
- package/src/local/browser-broker-server.mjs +5 -4
- package/src/local/browser-extension-identity.mjs +50 -0
- package/src/local/browser-extension-protocol.mjs +9 -4
- package/src/local/browser-operation-service.mjs +7 -0
- package/src/local/browser-pairing-http.mjs +36 -0
- package/src/local/browser-pairing-store.mjs +56 -47
- package/src/local/cli-account-admin.mjs +41 -7
- package/src/local/cli-activate.mjs +90 -0
- package/src/local/cli-approval.mjs +14 -58
- package/src/local/cli-local-admin.mjs +5 -15
- package/src/local/cli-options.mjs +11 -7
- package/src/local/cli-service.mjs +35 -5
- package/src/local/cli.mjs +112 -58
- package/src/local/daemon-process.mjs +6 -0
- package/src/local/delegated-process-sandbox.mjs +147 -0
- package/src/local/device-identity.mjs +157 -48
- package/src/local/device-root-provider.mjs +87 -0
- package/src/local/errors.mjs +25 -3
- package/src/local/git-service.mjs +16 -15
- package/src/local/job-runner.mjs +35 -27
- package/src/local/log.mjs +14 -1
- package/src/local/macos-trust-broker.mjs +320 -0
- package/src/local/managed-job-lock.mjs +18 -0
- package/src/local/managed-job-projection.mjs +9 -1
- package/src/local/managed-job-runner.mjs +13 -1
- package/src/local/managed-job-storage.mjs +2 -1
- package/src/local/managed-job-terminal.mjs +138 -0
- package/src/local/managed-jobs.mjs +88 -32
- package/src/local/operation-authorization.mjs +75 -239
- package/src/local/operation-risk.mjs +28 -0
- package/src/local/operation-state-lock.mjs +7 -86
- package/src/local/owner-state-lock.mjs +104 -0
- package/src/local/policy.mjs +19 -0
- package/src/local/process-execution.mjs +21 -9
- package/src/local/process-sessions.mjs +27 -17
- package/src/local/process-tracker.mjs +30 -6
- package/src/local/process-tree-ownership.mjs +59 -0
- package/src/local/process-tree.mjs +22 -18
- package/src/local/relay-connection.mjs +47 -35
- package/src/local/relay-diagnostics.mjs +65 -0
- package/src/local/remote-configuration.mjs +48 -0
- package/src/local/runtime-activation.mjs +141 -0
- package/src/local/runtime-diagnostics.mjs +21 -0
- package/src/local/runtime-relay.mjs +9 -8
- package/src/local/runtime-reporting.mjs +8 -3
- package/src/local/runtime-tool-handlers.mjs +8 -8
- package/src/local/runtime.mjs +73 -39
- package/src/local/secure-file.mjs +5 -1
- package/src/local/security-audit-log.mjs +204 -0
- package/src/local/service-convergence.mjs +17 -1
- package/src/local/service-ownership.mjs +17 -0
- package/src/local/service-restart-handoff.mjs +50 -0
- package/src/local/service-restart-scheduler.mjs +49 -0
- package/src/local/service-status.mjs +47 -0
- package/src/local/service.mjs +142 -43
- package/src/local/state-inventory.mjs +21 -3
- package/src/local/state.mjs +155 -17
- package/src/local/stdio.mjs +5 -0
- package/src/local/tool-executor.mjs +39 -6
- package/src/local/tool-result-boundary.mjs +51 -0
- package/src/local/tools.mjs +1 -0
- package/src/local/trusted-executable.mjs +53 -0
- package/src/local/trusted-git-executable.mjs +34 -0
- package/src/local/trusted-github-cli.mjs +24 -0
- package/src/local/windows-service.mjs +20 -0
- package/src/local/worker-deployment.mjs +3 -4
- package/src/local/worker-secret-file.mjs +2 -2
- package/src/local/workspace-file-service.mjs +34 -31
- package/src/shared/admin-auth.d.mts +2 -1
- package/src/shared/admin-auth.mjs +3 -3
- package/src/shared/device-session-auth.d.mts +20 -0
- package/src/shared/device-session-auth.mjs +51 -0
- package/src/shared/server-metadata.json +1 -1
- package/src/shared/tool-catalog.json +2 -2
- package/src/worker/account-admin.ts +23 -22
- package/src/worker/authority.ts +9 -2
- package/src/worker/daemon-auth.ts +45 -67
- package/src/worker/daemon-sockets.ts +12 -2
- package/src/worker/device-session-verifier.ts +129 -0
- package/src/worker/dpop.ts +151 -0
- package/src/worker/http.ts +47 -16
- package/src/worker/index.ts +53 -16
- package/src/worker/nonce-store.ts +1 -5
- package/src/worker/oauth-client-admin.ts +45 -0
- package/src/worker/oauth-controller.ts +55 -6
- package/src/worker/oauth-refresh-families.ts +21 -7
- package/src/worker/oauth-state.ts +6 -0
- package/src/worker/oauth-tokens.ts +28 -8
- package/src/worker/websocket-protocol.ts +8 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { BridgeError } from "./errors.mjs";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_HANDOFF_DELAY_MS = 300;
|
|
6
|
+
|
|
7
|
+
export function scheduleServiceRestart(options = {}) {
|
|
8
|
+
const platform = String(options.platform || process.platform);
|
|
9
|
+
if (platform === "win32") {
|
|
10
|
+
throw new BridgeError("unavailable", "in-process Windows service restart is not behavior-verified; run `machine-mcp service stop` and then `machine-mcp service start` from an independent terminal");
|
|
11
|
+
}
|
|
12
|
+
const spawnProcess = typeof options.spawnProcess === "function" ? options.spawnProcess : spawn;
|
|
13
|
+
const node = String(options.node || process.execPath);
|
|
14
|
+
const helper = String(options.helper || fileURLToPath(new URL("./service-restart-handoff.mjs", import.meta.url)));
|
|
15
|
+
const delayMs = boundedPositiveInteger(options.delayMs, DEFAULT_HANDOFF_DELAY_MS);
|
|
16
|
+
const child = spawnProcess(node, [helper, String(delayMs)], {
|
|
17
|
+
detached: true,
|
|
18
|
+
stdio: "ignore",
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
shell: false,
|
|
21
|
+
env: serviceControlEnvironment(options.env || process.env),
|
|
22
|
+
});
|
|
23
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
24
|
+
let settled = false;
|
|
25
|
+
const settle = (callback) => {
|
|
26
|
+
if (settled) return;
|
|
27
|
+
settled = true;
|
|
28
|
+
callback();
|
|
29
|
+
};
|
|
30
|
+
child.once("spawn", () => settle(() => {
|
|
31
|
+
child.unref?.();
|
|
32
|
+
resolvePromise({ ok: true, scheduled: true, delay_ms: delayMs });
|
|
33
|
+
}));
|
|
34
|
+
child.once("error", (error) => settle(() => rejectPromise(error)));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function serviceControlEnvironment(source = {}) {
|
|
39
|
+
const env = {};
|
|
40
|
+
for (const key of ["PATH", "HOME", "USERPROFILE", "SystemRoot", "WINDIR", "COMSPEC", "PATHEXT", "LANG", "LC_ALL", "LC_CTYPE"]) {
|
|
41
|
+
if (typeof source[key] === "string" && source[key]) env[key] = source[key];
|
|
42
|
+
}
|
|
43
|
+
return env;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function boundedPositiveInteger(value, fallback) {
|
|
47
|
+
const parsed = Number(value);
|
|
48
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
49
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const SYSTEMD_STATES = new Set(["active", "inactive", "failed", "activating", "deactivating", "reloading", "maintenance", "unknown"]);
|
|
2
|
+
|
|
3
|
+
export function launchdStatusSummary({ installed, definition, result }) {
|
|
4
|
+
const detail = String(result?.stdout || "");
|
|
5
|
+
const loaded = Number(result?.code) === 0;
|
|
6
|
+
const state = launchdField(detail, "state") || (loaded ? "loaded" : "inactive");
|
|
7
|
+
const pid = positiveMatch(detail, /\bpid = (\d+)/);
|
|
8
|
+
return {
|
|
9
|
+
ok: Boolean(installed),
|
|
10
|
+
provider: "launchd",
|
|
11
|
+
installed: Boolean(installed),
|
|
12
|
+
definition: String(definition || ""),
|
|
13
|
+
loaded,
|
|
14
|
+
active: loaded && (pid !== null || state === "running"),
|
|
15
|
+
state,
|
|
16
|
+
pid,
|
|
17
|
+
runs: positiveMatch(detail, /\bruns = (\d+)/),
|
|
18
|
+
last_termination_signal: textMatch(detail, /\blast terminating signal = ([^\n]+)/),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function systemdStatusSummary({ installed, definition, result }) {
|
|
23
|
+
const firstLine = String(result?.stdout || "").split(/\r?\n/, 1)[0].trim().toLowerCase();
|
|
24
|
+
const state = SYSTEMD_STATES.has(firstLine) ? firstLine : "unknown";
|
|
25
|
+
return {
|
|
26
|
+
ok: Boolean(installed),
|
|
27
|
+
provider: "systemd",
|
|
28
|
+
installed: Boolean(installed),
|
|
29
|
+
definition: String(definition || ""),
|
|
30
|
+
active: state === "active",
|
|
31
|
+
state,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function launchdField(value, field) {
|
|
36
|
+
const escaped = String(field || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
37
|
+
return textMatch(value, new RegExp(`\\b${escaped} = ([^\\n]+)`));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function textMatch(value, pattern) {
|
|
41
|
+
return pattern.exec(String(value || ""))?.[1]?.trim() || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function positiveMatch(value, pattern) {
|
|
45
|
+
const parsed = Number(textMatch(value, pattern));
|
|
46
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
47
|
+
}
|
package/src/local/service.mjs
CHANGED
|
@@ -5,10 +5,12 @@ import { runExecutable } from "./shell.mjs";
|
|
|
5
5
|
import { ensureOwnerOnlyDir, expandHome } from "./state.mjs";
|
|
6
6
|
import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
7
7
|
import { openRegularFileSync, readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
8
|
-
import { waitForInactiveStatus } from "./service-convergence.mjs";
|
|
8
|
+
import { waitForActiveStatus, waitForInactiveStatus, waitForStatus } from "./service-convergence.mjs";
|
|
9
|
+
import { launchdStatusSummary, systemdStatusSummary } from "./service-status.mjs";
|
|
9
10
|
import { writeServiceEnvironment } from "./service-environment.mjs";
|
|
10
11
|
import {
|
|
11
12
|
installWindowsTask,
|
|
13
|
+
restartWindowsTask,
|
|
12
14
|
startWindowsTask,
|
|
13
15
|
statusWindowsTask,
|
|
14
16
|
stopWindowsTask,
|
|
@@ -18,7 +20,7 @@ export { windowsCommandLineArgument } from "./windows-service.mjs";
|
|
|
18
20
|
|
|
19
21
|
const LABEL = "dev.machine-bridge-mcp.daemon";
|
|
20
22
|
const SERVICE_COMMAND_OUTPUT_BYTES = 64 * 1024;
|
|
21
|
-
const AUTOSTART_LOG_SCHEMA_VERSION =
|
|
23
|
+
const AUTOSTART_LOG_SCHEMA_VERSION = 4;
|
|
22
24
|
|
|
23
25
|
function serviceRun(command, args) {
|
|
24
26
|
return runServiceCommand(command, args);
|
|
@@ -57,7 +59,13 @@ export async function autostartStatus() {
|
|
|
57
59
|
export async function startAutostart({ logger = console } = {}) {
|
|
58
60
|
if (process.platform === "darwin") return startLaunchd(logger);
|
|
59
61
|
if (process.platform === "win32") return startWindowsTask(logger, { run: serviceRun });
|
|
60
|
-
return
|
|
62
|
+
return startSystemd(logger);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function restartAutostart({ logger = console } = {}) {
|
|
66
|
+
if (process.platform === "darwin") return restartLaunchd(logger);
|
|
67
|
+
if (process.platform === "win32") return restartWindowsTask(logger, { run: serviceRun });
|
|
68
|
+
return restartSystemd(logger);
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
export async function stopAutostart({ logger = console } = {}) {
|
|
@@ -84,33 +92,33 @@ export function trimAutostartLogs(stateRoot, options = {}) {
|
|
|
84
92
|
const keepBytes = Math.min(maxBytes, Number.isFinite(Number(options.keepBytes)) ? Math.max(1024, Number(options.keepBytes)) : 1024 * 1024);
|
|
85
93
|
const schemaVersion = String(AUTOSTART_LOG_SCHEMA_VERSION);
|
|
86
94
|
const logs = path.join(root, "logs");
|
|
95
|
+
ensureOwnerOnlyDir(logs);
|
|
87
96
|
const schemaFile = path.join(logs, ".log-schema");
|
|
88
97
|
const reset = readLogSchema(schemaFile) !== schemaVersion;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
label: "autostart log path",
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
const openedLogs = [];
|
|
99
|
+
try {
|
|
100
|
+
for (const name of ["daemon.out.log", "daemon.err.log"]) {
|
|
101
|
+
const file = path.join(logs, name);
|
|
102
|
+
ensurePrivateLogFile(file);
|
|
103
|
+
openedLogs.push(openRegularFileSync(file, fsConstants.O_RDWR, {
|
|
104
|
+
label: "autostart log path", chmod: 0o600, rejectMultipleLinks: true,
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
for (const opened of openedLogs) {
|
|
99
108
|
if (reset) {
|
|
100
|
-
ftruncateSync(fd, 0);
|
|
109
|
+
ftruncateSync(opened.fd, 0);
|
|
101
110
|
} else if (opened.info.size > maxBytes) {
|
|
102
|
-
const tail = readLogTail(fd, opened.info.size, keepBytes);
|
|
103
|
-
ftruncateSync(fd, 0);
|
|
104
|
-
if (tail.length) writeSync(fd, tail, 0, tail.length, 0);
|
|
111
|
+
const tail = readLogTail(opened.fd, opened.info.size, keepBytes);
|
|
112
|
+
ftruncateSync(opened.fd, 0);
|
|
113
|
+
if (tail.length) writeSync(opened.fd, tail, 0, tail.length, 0);
|
|
105
114
|
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
}
|
|
116
|
+
writePrivateServiceFile(schemaFile, `${schemaVersion}\n`);
|
|
117
|
+
} finally {
|
|
118
|
+
for (const opened of openedLogs) {
|
|
119
|
+
try { closeSync(opened.fd); } catch {}
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
|
-
|
|
113
|
-
try { writePrivateServiceFile(schemaFile, `${schemaVersion}\n`); } catch {}
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
function readLogSchema(file) {
|
|
@@ -179,26 +187,30 @@ export function stableNodeExecutable(options = {}) {
|
|
|
179
187
|
|
|
180
188
|
export function serviceEnvironmentPath(options = {}) {
|
|
181
189
|
const platform = String(options.platform || process.platform);
|
|
190
|
+
const pathApi = platform === "win32" ? path.win32 : path;
|
|
182
191
|
const delimiter = String(options.delimiter || (platform === "win32" ? ";" : ":"));
|
|
183
192
|
const pathEnv = String(options.pathEnv ?? process.env.PATH ?? "");
|
|
184
193
|
const node = String(options.node || process.execPath || "");
|
|
185
194
|
const entryScript = String(options.entryScript || "");
|
|
195
|
+
const entryDirectory = entryScript ? pathApi.dirname(pathApi.resolve(entryScript)) : "";
|
|
186
196
|
const defaults = platform === "darwin"
|
|
187
197
|
? ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"]
|
|
188
198
|
: platform === "win32"
|
|
189
199
|
? []
|
|
190
200
|
: ["/usr/local/bin", "/usr/bin", "/bin", "/usr/local/sbin", "/usr/sbin", "/sbin"];
|
|
201
|
+
const inheritedEntries = stripNpmLifecyclePathPrefix(pathEnv.split(delimiter));
|
|
191
202
|
const candidates = [
|
|
192
|
-
node ?
|
|
193
|
-
|
|
194
|
-
...
|
|
203
|
+
node ? pathApi.dirname(pathApi.resolve(node)) : "",
|
|
204
|
+
entryDirectory,
|
|
205
|
+
...inheritedEntries,
|
|
195
206
|
...defaults,
|
|
196
207
|
];
|
|
197
208
|
const seen = new Set();
|
|
198
209
|
const entries = [];
|
|
199
210
|
for (const raw of candidates) {
|
|
200
|
-
if (!raw || !
|
|
201
|
-
const normalized =
|
|
211
|
+
if (!raw || !pathApi.isAbsolute(raw)) continue;
|
|
212
|
+
const normalized = pathApi.resolve(raw);
|
|
213
|
+
if (isInactiveCandidateRuntimePath(normalized, entryDirectory, platform)) continue;
|
|
202
214
|
const key = platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
203
215
|
if (seen.has(key)) continue;
|
|
204
216
|
seen.add(key);
|
|
@@ -207,6 +219,33 @@ export function serviceEnvironmentPath(options = {}) {
|
|
|
207
219
|
return entries.join(delimiter);
|
|
208
220
|
}
|
|
209
221
|
|
|
222
|
+
function stripNpmLifecyclePathPrefix(entries) {
|
|
223
|
+
let lastMarker = -1;
|
|
224
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
225
|
+
const portable = String(entries[index] || "").replaceAll("\\", "/").toLowerCase();
|
|
226
|
+
if (portable.endsWith("/node-gyp-bin") && portable.includes("/@npmcli/run-script/")) lastMarker = index;
|
|
227
|
+
}
|
|
228
|
+
return lastMarker >= 0 ? entries.slice(lastMarker + 1) : entries;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isInactiveCandidateRuntimePath(candidate, entryDirectory, platform) {
|
|
232
|
+
if (!entryDirectory) return false;
|
|
233
|
+
const pathApi = platform === "win32" ? path.win32 : path;
|
|
234
|
+
const normalize = (value) => {
|
|
235
|
+
const resolved = pathApi.resolve(value);
|
|
236
|
+
return platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
237
|
+
};
|
|
238
|
+
const current = normalize(entryDirectory);
|
|
239
|
+
const portable = current.replaceAll("\\", "/");
|
|
240
|
+
const marker = "/release-channels/runtimes/";
|
|
241
|
+
const markerIndex = portable.indexOf(marker);
|
|
242
|
+
if (markerIndex < 0) return false;
|
|
243
|
+
const runtimeContainer = portable.slice(0, markerIndex + marker.length - 1);
|
|
244
|
+
const normalizedCandidate = normalize(candidate).replaceAll("\\", "/");
|
|
245
|
+
return normalizedCandidate !== portable
|
|
246
|
+
&& (normalizedCandidate === runtimeContainer || normalizedCandidate.startsWith(`${runtimeContainer}/`));
|
|
247
|
+
}
|
|
248
|
+
|
|
210
249
|
export function daemonArgs(spec) {
|
|
211
250
|
return [
|
|
212
251
|
spec.entryScript,
|
|
@@ -223,7 +262,7 @@ function ensurePrivateLogFile(file) {
|
|
|
223
262
|
const opened = openRegularFileSync(
|
|
224
263
|
file,
|
|
225
264
|
Number(fsConstants.O_WRONLY) | Number(fsConstants.O_CREAT) | Number(fsConstants.O_APPEND),
|
|
226
|
-
{ label: "autostart log path", mode: 0o600, chmod: 0o600 },
|
|
265
|
+
{ label: "autostart log path", mode: 0o600, chmod: 0o600, rejectMultipleLinks: true },
|
|
227
266
|
);
|
|
228
267
|
closeSync(opened.fd);
|
|
229
268
|
}
|
|
@@ -260,16 +299,53 @@ async function installLaunchd(spec, logger) {
|
|
|
260
299
|
|
|
261
300
|
async function startLaunchd(logger) {
|
|
262
301
|
const plistPath = launchdPlistPath();
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
302
|
+
const domainTarget = launchdDomainTarget();
|
|
303
|
+
const serviceTarget = launchdServiceTarget();
|
|
304
|
+
if (!existsSync(plistPath)) return { ok: false, provider: "launchd", installed: false, loaded: false, active: false, reason: "not_installed" };
|
|
305
|
+
const before = await statusLaunchd();
|
|
306
|
+
if (before.active) {
|
|
307
|
+
logger.info?.("launchd service is already running");
|
|
308
|
+
return { ...before, ok: true, active_before: true, already_running: true, reason: "already_running" };
|
|
309
|
+
}
|
|
310
|
+
const boot = before.loaded
|
|
311
|
+
? { code: 0, stdout: "", stderr: "", already_loaded: true }
|
|
312
|
+
: await serviceRun("launchctl", ["bootstrap", domainTarget, plistPath]);
|
|
313
|
+
const kick = await serviceRun("launchctl", ["kickstart", serviceTarget]);
|
|
314
|
+
const after = await waitForActiveStatus(statusLaunchd);
|
|
315
|
+
const ok = after?.active === true;
|
|
270
316
|
if (ok) logger.info?.("launchd service started");
|
|
271
317
|
else logger.warn?.("launchd service failed to start");
|
|
272
|
-
return {
|
|
318
|
+
return {
|
|
319
|
+
ok,
|
|
320
|
+
provider: "launchd",
|
|
321
|
+
installed: true,
|
|
322
|
+
loaded: after?.loaded === true,
|
|
323
|
+
active_before: false,
|
|
324
|
+
active: after?.active === true,
|
|
325
|
+
bootstrap: boot,
|
|
326
|
+
kickstart: kick,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function restartLaunchd(logger) {
|
|
331
|
+
const plistPath = launchdPlistPath();
|
|
332
|
+
if (!existsSync(plistPath)) return { ok: false, provider: "launchd", installed: false, reason: "not_installed" };
|
|
333
|
+
const before = await statusLaunchd();
|
|
334
|
+
if (!before.active) {
|
|
335
|
+
const started = await startLaunchd(logger);
|
|
336
|
+
return { ...started, restarted: false, reason: started.ok ? "started_inactive_service" : started.reason || "start_failed" };
|
|
337
|
+
}
|
|
338
|
+
const target = launchdServiceTarget();
|
|
339
|
+
const previousPid = before.pid;
|
|
340
|
+
const kick = await serviceRun("launchctl", ["kickstart", "-k", target]);
|
|
341
|
+
const after = await waitForStatus(
|
|
342
|
+
statusLaunchd,
|
|
343
|
+
(status) => status?.active === true && (!previousPid || status.pid !== previousPid),
|
|
344
|
+
);
|
|
345
|
+
const ok = kick.code === 0 && after?.active === true && (!previousPid || after.pid !== previousPid);
|
|
346
|
+
if (ok) logger.info?.("launchd service restarted");
|
|
347
|
+
else logger.warn?.("launchd service restart could not be verified");
|
|
348
|
+
return { ok, provider: "launchd", installed: true, active_before: true, active: after?.active === true, restarted: ok, kickstart: kick };
|
|
273
349
|
}
|
|
274
350
|
|
|
275
351
|
async function stopLaunchd(logger) {
|
|
@@ -277,12 +353,13 @@ async function stopLaunchd(logger) {
|
|
|
277
353
|
const domainTarget = launchdDomainTarget();
|
|
278
354
|
const serviceTarget = launchdServiceTarget();
|
|
279
355
|
const before = await statusLaunchd();
|
|
280
|
-
if (!before.
|
|
356
|
+
if (!before.loaded) {
|
|
281
357
|
logger.info?.("launchd service is not loaded");
|
|
282
358
|
return {
|
|
283
359
|
ok: true,
|
|
284
360
|
provider: "launchd",
|
|
285
361
|
installed: existsSync(plistPath),
|
|
362
|
+
loaded: false,
|
|
286
363
|
active_before: false,
|
|
287
364
|
active: false,
|
|
288
365
|
already_stopped: true,
|
|
@@ -327,9 +404,8 @@ async function uninstallLaunchd(logger) {
|
|
|
327
404
|
|
|
328
405
|
async function statusLaunchd() {
|
|
329
406
|
const plistPath = launchdPlistPath();
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
return { ok: existsSync(plistPath), provider: "launchd", installed: existsSync(plistPath), path: plistPath, active: result.code === 0, detail: result.stdout || result.stderr };
|
|
407
|
+
const result = await serviceRun("launchctl", ["print", launchdServiceTarget()]);
|
|
408
|
+
return launchdStatusSummary({ installed: existsSync(plistPath), definition: LABEL, result });
|
|
333
409
|
}
|
|
334
410
|
|
|
335
411
|
export function launchdPlist({ args, pathEnv, stdout, stderr }) {
|
|
@@ -393,10 +469,33 @@ ${disable.stderr}`));
|
|
|
393
469
|
return { ok, provider: "systemd", path: servicePath, disable, active_check: activeCheck, reload, active: false };
|
|
394
470
|
}
|
|
395
471
|
|
|
472
|
+
async function startSystemd(logger) {
|
|
473
|
+
const before = await statusSystemd();
|
|
474
|
+
if (before.active) {
|
|
475
|
+
logger.info?.("systemd service is already running");
|
|
476
|
+
return { ...before, ok: true, active_before: true, already_running: true, reason: "already_running" };
|
|
477
|
+
}
|
|
478
|
+
const command = await serviceRun("systemctl", ["--user", "start", "machine-bridge-mcp.service"]);
|
|
479
|
+
const after = await waitForActiveStatus(statusSystemd);
|
|
480
|
+
const ok = command.code === 0 && after.active === true;
|
|
481
|
+
if (ok) logger.info?.("systemd service started");
|
|
482
|
+
else logger.warn?.("systemd service failed to start");
|
|
483
|
+
return { ok, provider: "systemd", installed: after.installed, active_before: false, active: after.active, command };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function restartSystemd(logger) {
|
|
487
|
+
const command = await serviceRun("systemctl", ["--user", "restart", "machine-bridge-mcp.service"]);
|
|
488
|
+
const after = await waitForActiveStatus(statusSystemd);
|
|
489
|
+
const ok = command.code === 0 && after.active === true;
|
|
490
|
+
if (ok) logger.info?.("systemd service restarted");
|
|
491
|
+
else logger.warn?.("systemd service restart could not be verified");
|
|
492
|
+
return { ok, provider: "systemd", installed: after.installed, active: after.active, restarted: ok, command };
|
|
493
|
+
}
|
|
494
|
+
|
|
396
495
|
async function statusSystemd() {
|
|
397
496
|
const servicePath = systemdPath();
|
|
398
|
-
const result = await serviceRun("systemctl", ["--user", "
|
|
399
|
-
return {
|
|
497
|
+
const result = await serviceRun("systemctl", ["--user", "is-active", "machine-bridge-mcp.service"]);
|
|
498
|
+
return systemdStatusSummary({ installed: existsSync(servicePath), definition: "machine-bridge-mcp.service", result });
|
|
400
499
|
}
|
|
401
500
|
|
|
402
501
|
export function systemdUnit(spec) {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { existsSync, readdirSync, realpathSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { activeManagedJobs } from "./managed-jobs.mjs";
|
|
4
|
+
import { activeManagedJobLock } from "./managed-job-lock.mjs";
|
|
4
5
|
import { inspectProcessInstance } from "./process-identity.mjs";
|
|
5
6
|
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
6
7
|
import { STATE_SCHEMA_VERSION, expandHome, readDaemonLockOwner, resolveWorkspace } from "./state.mjs";
|
|
7
8
|
|
|
8
9
|
const PROFILE_NAME = /^[a-f0-9]{24}$/;
|
|
10
|
+
const JOB_ID = /^job_[A-Za-z0-9_-]{24,}$/;
|
|
9
11
|
const WORKER_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
10
12
|
const MAX_STATE_BYTES = 2 * 1024 * 1024;
|
|
11
13
|
|
|
@@ -17,7 +19,7 @@ export function knownWorkerNames(stateRoot) {
|
|
|
17
19
|
const profileDir = resolve(profiles, entry.name);
|
|
18
20
|
const stateFile = resolve(profileDir, "state.json");
|
|
19
21
|
if (!existsSync(stateFile)) {
|
|
20
|
-
const evidence = readdirSync(profileDir).some((name) => /^state\.json\.corrupt-/.test(name) || name === "daemon.lock");
|
|
22
|
+
const evidence = readdirSync(profileDir).some((name) => /^state\.json\.corrupt-/.test(name) || name === "state.json.recovery-required" || name === "daemon.lock");
|
|
21
23
|
if (evidence) throw unreadableWorkerState(entry.name);
|
|
22
24
|
continue;
|
|
23
25
|
}
|
|
@@ -101,8 +103,14 @@ export function activeStateLocks(stateRoot) {
|
|
|
101
103
|
if (!existsSync(profiles)) return [];
|
|
102
104
|
const active = [];
|
|
103
105
|
for (const profile of profileDirectories(profiles)) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
const profileDir = resolve(profiles, profile.name);
|
|
107
|
+
for (const [kind, name] of [
|
|
108
|
+
["daemon", "daemon.lock"],
|
|
109
|
+
["startup", "startup.lock"],
|
|
110
|
+
["operation-authorization", "operation-authorization.lock"],
|
|
111
|
+
["security-audit", "security-audit.lock"],
|
|
112
|
+
]) {
|
|
113
|
+
const lockPath = resolve(profileDir, name);
|
|
106
114
|
if (!existsSync(lockPath)) continue;
|
|
107
115
|
const owner = readDaemonLockOwner(lockPath);
|
|
108
116
|
if (!owner) {
|
|
@@ -115,6 +123,16 @@ export function activeStateLocks(stateRoot) {
|
|
|
115
123
|
active.push({ kind, pid: owner.pid, path: lockPath, reason: identity.reason });
|
|
116
124
|
}
|
|
117
125
|
}
|
|
126
|
+
const jobRoot = resolve(profileDir, "jobs");
|
|
127
|
+
if (!existsSync(jobRoot)) continue;
|
|
128
|
+
for (const entry of readdirSync(jobRoot, { withFileTypes: true })) {
|
|
129
|
+
if (!entry.isDirectory() || !JOB_ID.test(entry.name)) continue;
|
|
130
|
+
for (const [kind, name] of [["job-transition", "transition.lock"], ["job-recovery", "recovery.lock"]]) {
|
|
131
|
+
const lockPath = resolve(jobRoot, entry.name, name);
|
|
132
|
+
const lock = activeManagedJobLock(lockPath);
|
|
133
|
+
if (lock?.active) active.push({ kind, pid: lock.pid, path: lockPath, reason: lock.reason, job_id: entry.name });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
118
136
|
}
|
|
119
137
|
return active;
|
|
120
138
|
}
|