machine-bridge-mcp 0.4.2 → 0.6.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 +51 -0
- package/README.md +110 -14
- package/SECURITY.md +38 -6
- package/docs/ARCHITECTURE.md +50 -15
- package/docs/CLIENTS.md +18 -1
- package/docs/LOGGING.md +82 -0
- package/docs/MANAGED_JOBS.md +274 -0
- package/docs/OPERATIONS.md +80 -23
- package/docs/TESTING.md +13 -5
- package/package.json +8 -7
- package/src/local/cli.mjs +345 -80
- package/src/local/daemon.mjs +159 -42
- package/src/local/job-runner.mjs +470 -0
- package/src/local/log.mjs +56 -15
- package/src/local/managed-jobs.mjs +854 -0
- package/src/local/service.mjs +40 -21
- package/src/local/state.mjs +94 -26
- package/src/local/stdio.mjs +14 -38
- package/src/local/tools.mjs +28 -13
- package/src/shared/server-metadata.json +24 -0
- package/src/shared/tool-catalog.json +500 -2
- package/src/worker/index.ts +13 -11
package/src/local/cli.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
|
|
3
|
-
import path, { resolve } from "node:path";
|
|
3
|
+
import path, { join, resolve } from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import readline from "node:readline/promises";
|
|
6
6
|
import { LocalDaemon } from "./daemon.mjs";
|
|
7
7
|
import { runStdioServer } from "./stdio.mjs";
|
|
8
|
-
import { normalizePolicy } from "./tools.mjs";
|
|
9
|
-
import { createLogger, sanitizeLogText } from "./log.mjs";
|
|
8
|
+
import { DEFAULT_POLICY_PROFILE, DEFAULT_POLICY_REVISION, POLICY_PROFILES, normalizePolicy, policyProfile } from "./tools.mjs";
|
|
9
|
+
import { classifyOperationalError, createLogger, normalizeLogLevel, sanitizeLogText } from "./log.mjs";
|
|
10
|
+
import { activeManagedJobs, inspectResourceFile, loadManagedJobPlan, ManagedJobManager, publicResourceRegistry, validateResourceName } from "./managed-jobs.mjs";
|
|
10
11
|
import { runWrangler } from "./shell.mjs";
|
|
11
12
|
import {
|
|
12
13
|
acquireDaemonLock,
|
|
@@ -37,10 +38,10 @@ const BOOLEAN_OPTIONS = new Set([
|
|
|
37
38
|
"help", "version", "quiet", "json", "verbose", "rotateSecrets", "forceWorker",
|
|
38
39
|
"daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials",
|
|
39
40
|
"printCredentials", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
|
|
40
|
-
"yes", "keepWorker", "
|
|
41
|
+
"yes", "keepWorker", "allowInsecurePermissions",
|
|
41
42
|
]);
|
|
42
43
|
const VALUE_OPTIONS = new Set([
|
|
43
|
-
"workspace", "stateDir", "workerName", "profile", "execMode", "client", "
|
|
44
|
+
"workspace", "stateDir", "workerName", "profile", "execMode", "client", "logLevel",
|
|
44
45
|
]);
|
|
45
46
|
|
|
46
47
|
export async function main(argv = process.argv.slice(2)) {
|
|
@@ -51,6 +52,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
51
52
|
if (command === "api") throw removedLocalApiError();
|
|
52
53
|
validateCommandOptions(command, args);
|
|
53
54
|
validatePositionals(command, args);
|
|
55
|
+
validateLoggingOptions(args);
|
|
54
56
|
|
|
55
57
|
switch (command) {
|
|
56
58
|
case "start": return startCommand(args);
|
|
@@ -62,6 +64,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
62
64
|
case "service":
|
|
63
65
|
case "autostart": return serviceCommand(args);
|
|
64
66
|
case "rotate-secrets": return rotateSecretsCommand(args);
|
|
67
|
+
case "resource": return resourceCommand(args);
|
|
68
|
+
case "job": return jobCommand(args);
|
|
65
69
|
case "uninstall": return uninstallCommand(args);
|
|
66
70
|
default:
|
|
67
71
|
console.error(`Unknown command: ${command}`);
|
|
@@ -72,19 +76,20 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
72
76
|
|
|
73
77
|
const COMMAND_OPTIONS = {
|
|
74
78
|
start: new Set([
|
|
75
|
-
"workspace", "stateDir", "workerName", "quiet", "json", "verbose", "rotateSecrets", "forceWorker",
|
|
79
|
+
"workspace", "stateDir", "workerName", "quiet", "json", "verbose", "logLevel", "rotateSecrets", "forceWorker",
|
|
76
80
|
"daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials", "printCredentials",
|
|
77
81
|
"profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
|
|
78
|
-
"noApi", "api", "apiPort", "apiHost", "apiKey", "rotateApiKey", "apiModel", "port",
|
|
79
82
|
]),
|
|
80
|
-
stdio: new Set(["workspace", "stateDir", "profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths", "verbose"]),
|
|
83
|
+
stdio: new Set(["workspace", "stateDir", "profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths", "verbose", "quiet", "logLevel"]),
|
|
81
84
|
"client-config": new Set(["workspace", "stateDir", "profile", "client", "json"]),
|
|
82
85
|
status: new Set(["workspace", "stateDir"]),
|
|
83
86
|
doctor: new Set(["workspace", "stateDir"]),
|
|
84
|
-
"rotate-secrets": new Set(["workspace", "stateDir", "workerName", "noPrintCredentials", "quiet"]),
|
|
87
|
+
"rotate-secrets": new Set(["workspace", "stateDir", "workerName", "noPrintCredentials", "printMcpCredentials", "printCredentials", "quiet"]),
|
|
85
88
|
workspace: new Set(["workspace", "stateDir"]),
|
|
86
89
|
service: new Set(["workspace", "stateDir", "quiet"]),
|
|
87
90
|
autostart: new Set(["workspace", "stateDir", "quiet"]),
|
|
91
|
+
resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "json"]),
|
|
92
|
+
job: new Set(["workspace", "stateDir", "json", "yes"]),
|
|
88
93
|
uninstall: new Set(["stateDir", "keepWorker", "yes"]),
|
|
89
94
|
};
|
|
90
95
|
|
|
@@ -97,14 +102,27 @@ export function validateCommandOptions(command, args) {
|
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
export function validateLoggingOptions(args = {}) {
|
|
106
|
+
if (args.quiet && args.verbose) throw new Error("--quiet and --verbose cannot be used together");
|
|
107
|
+
if (args.logLevel !== undefined && (args.quiet || args.verbose)) {
|
|
108
|
+
throw new Error("--log-level cannot be combined with --quiet or --verbose");
|
|
109
|
+
}
|
|
110
|
+
if (args.logLevel !== undefined) normalizeLogLevel(args.logLevel);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function effectiveLogLevel(args = {}) {
|
|
114
|
+
if (args.logLevel !== undefined) return normalizeLogLevel(args.logLevel);
|
|
115
|
+
if (args.quiet) return "error";
|
|
116
|
+
if (args.verbose) return "debug";
|
|
117
|
+
return "info";
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
function toKebab(value) {
|
|
101
121
|
return String(value).replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
export function validatePositionals(command, args) {
|
|
105
125
|
const count = args._.length;
|
|
106
|
-
const conflict = Boolean(args.workspace) && count > (command === "workspace" || command === "service" || command === "autostart" ? 1 : 0);
|
|
107
|
-
if (conflict) throw new Error("workspace path was provided both positionally and with --workspace");
|
|
108
126
|
if (["start", "stdio", "status", "doctor", "rotate-secrets"].includes(command)) {
|
|
109
127
|
if (count > 1) throw new Error(`${command} accepts at most one positional workspace path`);
|
|
110
128
|
if (args.workspace && count) throw new Error("workspace path was provided both positionally and with --workspace");
|
|
@@ -128,6 +146,18 @@ export function validatePositionals(command, args) {
|
|
|
128
146
|
if (count > 1) throw new Error("client-config accepts at most one positional client name");
|
|
129
147
|
return;
|
|
130
148
|
}
|
|
149
|
+
if (command === "resource") {
|
|
150
|
+
const action = String(args._[0] || "list");
|
|
151
|
+
const max = action === "add" ? 3 : action === "remove" || action === "check" ? 2 : 1;
|
|
152
|
+
if (count > max) throw new Error(`resource ${action} received too many positional arguments`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (command === "job") {
|
|
156
|
+
const action = String(args._[0] || "list");
|
|
157
|
+
const max = action === "read" || action === "inspect" || action === "cancel" || action === "approve" || action === "submit" ? 2 : 1;
|
|
158
|
+
if (count > max) throw new Error(`job ${action} received too many positional arguments`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
131
161
|
if (command === "uninstall" && count) throw new Error("uninstall does not accept positional arguments");
|
|
132
162
|
}
|
|
133
163
|
|
|
@@ -256,8 +286,7 @@ async function confirm(prompt, assumeYes = false) {
|
|
|
256
286
|
|
|
257
287
|
async function startCommand(args) {
|
|
258
288
|
assertNodeVersion();
|
|
259
|
-
|
|
260
|
-
const logger = createLogger({ quiet: Boolean(args.quiet || args.json), verbose: Boolean(args.verbose), component: "cli" });
|
|
289
|
+
const logger = createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "cli" });
|
|
261
290
|
const workspace = await chooseWorkspace(args, { promptOnFirstRun: true, save: true, allowPositional: true });
|
|
262
291
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
263
292
|
const startupLock = acquireStartupLock(state);
|
|
@@ -273,7 +302,7 @@ async function startCommand(args) {
|
|
|
273
302
|
} else {
|
|
274
303
|
// Stop an installed service before acquiring the runtime lock. If a
|
|
275
304
|
// foreground daemon owns the lock, no new policy or secret state is saved.
|
|
276
|
-
await stopAutostartBestEffort(
|
|
305
|
+
await stopAutostartBestEffort(logger);
|
|
277
306
|
}
|
|
278
307
|
|
|
279
308
|
const lock = acquireDaemonLock(state);
|
|
@@ -281,7 +310,7 @@ async function startCommand(args) {
|
|
|
281
310
|
const pid = lock.owner?.pid ? `pid ${lock.owner.pid}` : "unknown pid";
|
|
282
311
|
logger.warn(`local daemon already running for this workspace (${pid}); requested changes were not applied`);
|
|
283
312
|
if (args.json) printStartJson(state, {
|
|
284
|
-
|
|
313
|
+
showCredentials: Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials),
|
|
285
314
|
requestedChangesApplied: false,
|
|
286
315
|
notice: "local daemon already running; requested changes were not applied",
|
|
287
316
|
});
|
|
@@ -289,6 +318,7 @@ async function startCommand(args) {
|
|
|
289
318
|
noPrintCredentials: Boolean(args.noPrintCredentials),
|
|
290
319
|
includeCredentials: Boolean(args.printMcpCredentials || args.printCredentials),
|
|
291
320
|
quiet: Boolean(args.quiet),
|
|
321
|
+
verbose: Boolean(args.verbose),
|
|
292
322
|
});
|
|
293
323
|
return;
|
|
294
324
|
}
|
|
@@ -299,7 +329,9 @@ async function startCommand(args) {
|
|
|
299
329
|
const firstMcpConnection = !previousMcpServerUrl || !state.worker?.oauthPassword;
|
|
300
330
|
const workerName = validateWorkerName(args.workerName);
|
|
301
331
|
ensureWorkerSecrets(state, { rotateSecrets: Boolean(args.rotateSecrets), workerName });
|
|
332
|
+
const previousPolicyOrigin = state.policy?.origin;
|
|
302
333
|
state.policy = resolvePolicy(args, state.policy);
|
|
334
|
+
const policyMigrated = !previousPolicyOrigin && state.policy.origin === "migrated";
|
|
303
335
|
state.policy.updatedAt = new Date().toISOString();
|
|
304
336
|
saveState(state);
|
|
305
337
|
|
|
@@ -307,10 +339,10 @@ async function startCommand(args) {
|
|
|
307
339
|
else if (!state.worker.url) throw new Error("--daemon-only requires an existing worker URL in state; run start once without --daemon-only");
|
|
308
340
|
|
|
309
341
|
const mcpConnectionChanged = previousMcpServerUrl && previousMcpServerUrl !== state.worker.mcpServerUrl;
|
|
310
|
-
const shouldPrintMcpCredentials = Boolean(args.
|
|
342
|
+
const shouldPrintMcpCredentials = Boolean(args.printMcpCredentials || args.printCredentials || firstMcpConnection || args.rotateSecrets || mcpConnectionChanged);
|
|
311
343
|
|
|
312
344
|
if (!args.daemonOnly && !args.noAutostart) {
|
|
313
|
-
await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1] });
|
|
345
|
+
await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1], logger });
|
|
314
346
|
}
|
|
315
347
|
|
|
316
348
|
daemon = new LocalDaemon({
|
|
@@ -318,7 +350,10 @@ async function startCommand(args) {
|
|
|
318
350
|
secret: state.worker.daemonSecret,
|
|
319
351
|
workspace,
|
|
320
352
|
policy: state.policy,
|
|
321
|
-
logger: createLogger({
|
|
353
|
+
logger: createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "daemon" }),
|
|
354
|
+
jobRoot: join(state.paths.profileDir, "jobs"),
|
|
355
|
+
resources: state.resources,
|
|
356
|
+
resourceStatePath: state.paths.statePath,
|
|
322
357
|
onSuperseded: () => {
|
|
323
358
|
logger.warn("this daemon was replaced by a newer authenticated instance; exiting without reconnecting");
|
|
324
359
|
lock.release();
|
|
@@ -327,13 +362,18 @@ async function startCommand(args) {
|
|
|
327
362
|
});
|
|
328
363
|
|
|
329
364
|
const waitForConnect = daemon.start();
|
|
330
|
-
await waitForConnectWithNotice(waitForConnect, 20_000,
|
|
331
|
-
if (args.json) printStartJson(state, {
|
|
365
|
+
await waitForConnectWithNotice(waitForConnect, 20_000, logger);
|
|
366
|
+
if (args.json) printStartJson(state, {
|
|
367
|
+
showCredentials: Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials),
|
|
368
|
+
notice: policyMigrated ? "legacy implicit policy migrated to full access" : "",
|
|
369
|
+
});
|
|
332
370
|
else {
|
|
333
371
|
printMcpConnection(state, {
|
|
334
372
|
noPrintCredentials: Boolean(args.noPrintCredentials),
|
|
335
373
|
includeCredentials: shouldPrintMcpCredentials,
|
|
336
374
|
quiet: Boolean(args.quiet),
|
|
375
|
+
verbose: Boolean(args.verbose),
|
|
376
|
+
policyMigrated,
|
|
337
377
|
});
|
|
338
378
|
}
|
|
339
379
|
keepProcessAlive({ daemon, lock, logger });
|
|
@@ -347,13 +387,6 @@ async function startCommand(args) {
|
|
|
347
387
|
}
|
|
348
388
|
}
|
|
349
389
|
|
|
350
|
-
const POLICY_PROFILES = Object.freeze({
|
|
351
|
-
review: Object.freeze({ profile: "review", allowWrite: false, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
|
|
352
|
-
edit: Object.freeze({ profile: "edit", allowWrite: true, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
|
|
353
|
-
agent: Object.freeze({ profile: "agent", allowWrite: true, execMode: "direct", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
|
|
354
|
-
full: Object.freeze({ profile: "full", allowWrite: true, execMode: "shell", unrestrictedPaths: true, minimalEnv: false, exposeAbsolutePaths: true }),
|
|
355
|
-
});
|
|
356
|
-
|
|
357
390
|
export function resolvePolicy(args = {}, stored = {}) {
|
|
358
391
|
const hasStored = stored && typeof stored === "object" && (
|
|
359
392
|
typeof stored.allowWrite === "boolean" || typeof stored.allowExec === "boolean" || typeof stored.execMode === "string"
|
|
@@ -361,14 +394,15 @@ export function resolvePolicy(args = {}, stored = {}) {
|
|
|
361
394
|
const explicitKeys = ["profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths"];
|
|
362
395
|
const hasExplicit = explicitKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key));
|
|
363
396
|
let base;
|
|
397
|
+
|
|
364
398
|
if (args.profile !== undefined) {
|
|
365
399
|
const profile = String(args.profile).trim().toLowerCase();
|
|
366
400
|
if (!POLICY_PROFILES[profile]) throw new Error(`--profile must be one of: ${Object.keys(POLICY_PROFILES).join(", ")}`);
|
|
367
|
-
base =
|
|
401
|
+
base = policyProfile(profile, "explicit");
|
|
368
402
|
} else if (hasStored) {
|
|
369
|
-
base =
|
|
403
|
+
base = migrateLegacyPolicy(stored);
|
|
370
404
|
} else {
|
|
371
|
-
base =
|
|
405
|
+
base = policyProfile(DEFAULT_POLICY_PROFILE, "default");
|
|
372
406
|
}
|
|
373
407
|
|
|
374
408
|
if (!hasExplicit) return normalizePolicy(base);
|
|
@@ -388,16 +422,167 @@ export function resolvePolicy(args = {}, stored = {}) {
|
|
|
388
422
|
if (args.absolutePaths === true) base.exposeAbsolutePaths = true;
|
|
389
423
|
if (args.absolutePaths === false) base.exposeAbsolutePaths = false;
|
|
390
424
|
const overrideKeys = ["execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths"];
|
|
391
|
-
if (args.profile === undefined || overrideKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key)))
|
|
425
|
+
if (args.profile === undefined || overrideKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key))) {
|
|
426
|
+
base.profile = "custom";
|
|
427
|
+
base.origin = "custom";
|
|
428
|
+
base.revision = DEFAULT_POLICY_REVISION;
|
|
429
|
+
}
|
|
392
430
|
return normalizePolicy(base);
|
|
393
431
|
}
|
|
394
432
|
|
|
433
|
+
function migrateLegacyPolicy(stored = {}) {
|
|
434
|
+
if (stored.origin === "default" && Number(stored.revision || 0) < DEFAULT_POLICY_REVISION) {
|
|
435
|
+
return policyProfile(DEFAULT_POLICY_PROFILE, "default");
|
|
436
|
+
}
|
|
437
|
+
if (stored.origin === "migrated" && Number(stored.revision || 0) < DEFAULT_POLICY_REVISION) {
|
|
438
|
+
return policyProfile(DEFAULT_POLICY_PROFILE, "migrated");
|
|
439
|
+
}
|
|
440
|
+
if (stored.origin) return normalizePolicy(stored);
|
|
441
|
+
const normalized = normalizePolicy(stored);
|
|
442
|
+
const looksLikeLegacyImplicitDefault = (
|
|
443
|
+
normalized.profile === "custom" &&
|
|
444
|
+
normalized.allowWrite === true &&
|
|
445
|
+
normalized.execMode === "shell" &&
|
|
446
|
+
normalized.unrestrictedPaths === false &&
|
|
447
|
+
normalized.minimalEnv === true &&
|
|
448
|
+
normalized.exposeAbsolutePaths === false
|
|
449
|
+
);
|
|
450
|
+
if (looksLikeLegacyImplicitDefault) return policyProfile("full", "migrated");
|
|
451
|
+
return normalizePolicy({ ...normalized, origin: "legacy-preserved", revision: DEFAULT_POLICY_REVISION });
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
|
|
395
455
|
async function stdioCommand(args) {
|
|
396
456
|
assertNodeVersion();
|
|
397
457
|
const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
|
|
398
458
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
399
459
|
const policy = resolvePolicy(args, state.policy);
|
|
400
|
-
await runStdioServer({
|
|
460
|
+
await runStdioServer({
|
|
461
|
+
workspace,
|
|
462
|
+
policy,
|
|
463
|
+
logLevel: effectiveLogLevel(args),
|
|
464
|
+
jobRoot: join(state.paths.profileDir, "jobs"),
|
|
465
|
+
resources: state.resources,
|
|
466
|
+
resourceStatePath: state.paths.statePath,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function resourceCommand(args) {
|
|
471
|
+
const action = String(args._[0] || "list").toLowerCase();
|
|
472
|
+
const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
|
|
473
|
+
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
474
|
+
state.resources ||= {};
|
|
475
|
+
|
|
476
|
+
if (action === "list") {
|
|
477
|
+
const resources = publicResourceRegistry(state.resources);
|
|
478
|
+
if (args.json) console.log(JSON.stringify({ workspace, resources }, null, 2));
|
|
479
|
+
else if (!Object.keys(resources).length) console.log("No local resources registered.");
|
|
480
|
+
else for (const [name, value] of Object.entries(resources)) console.log(`${name} ${value.path} ${value.mode || "n/a"} ${value.size ?? "n/a"} bytes`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (action === "add") {
|
|
485
|
+
const name = validateResourceName(args._[1]);
|
|
486
|
+
const inputPath = args._[2];
|
|
487
|
+
if (!inputPath) throw new Error("resource add requires NAME and FILE_PATH");
|
|
488
|
+
const lock = acquireStartupLock(state);
|
|
489
|
+
if (!lock.acquired) throw new Error("another state-changing operation is already running for this workspace");
|
|
490
|
+
try {
|
|
491
|
+
const latest = loadState(workspace, { stateDir: args.stateDir });
|
|
492
|
+
latest.resources ||= {};
|
|
493
|
+
const inspected = inspectResourceFile(expandHome(inputPath), { allowInsecurePermissions: args.allowInsecurePermissions === true });
|
|
494
|
+
if (!Object.prototype.hasOwnProperty.call(latest.resources, name) && Object.keys(latest.resources).length >= 64) {
|
|
495
|
+
throw new Error("local resource registry limit reached (64)");
|
|
496
|
+
}
|
|
497
|
+
latest.resources[name] = inspected;
|
|
498
|
+
saveState(latest);
|
|
499
|
+
const result = { name, ...inspected, contents_exposed: false, available_to_new_jobs_immediately: true };
|
|
500
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
501
|
+
else {
|
|
502
|
+
console.log(`Registered local resource: ${name}`);
|
|
503
|
+
console.log(`Path: ${inspected.path}`);
|
|
504
|
+
console.log(`Mode: ${inspected.mode || "n/a"}; size: ${inspected.size} bytes`);
|
|
505
|
+
console.log("The resource is available to newly submitted managed jobs immediately.");
|
|
506
|
+
}
|
|
507
|
+
} finally {
|
|
508
|
+
lock.release();
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (action === "remove") {
|
|
514
|
+
const name = validateResourceName(args._[1]);
|
|
515
|
+
const lock = acquireStartupLock(state);
|
|
516
|
+
if (!lock.acquired) throw new Error("another state-changing operation is already running for this workspace");
|
|
517
|
+
try {
|
|
518
|
+
const latest = loadState(workspace, { stateDir: args.stateDir });
|
|
519
|
+
latest.resources ||= {};
|
|
520
|
+
const existed = Object.prototype.hasOwnProperty.call(latest.resources, name);
|
|
521
|
+
delete latest.resources[name];
|
|
522
|
+
saveState(latest);
|
|
523
|
+
const result = { name, removed: existed, affects_new_jobs_immediately: true };
|
|
524
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
525
|
+
else {
|
|
526
|
+
console.log(existed ? `Removed local resource: ${name}` : `Local resource was not registered: ${name}`);
|
|
527
|
+
console.log("The change applies to newly submitted managed jobs immediately.");
|
|
528
|
+
}
|
|
529
|
+
} finally {
|
|
530
|
+
lock.release();
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (action === "check") {
|
|
536
|
+
const name = validateResourceName(args._[1]);
|
|
537
|
+
const resource = state.resources[name];
|
|
538
|
+
if (!resource) throw new Error(`local resource is not registered: ${name}`);
|
|
539
|
+
const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true });
|
|
540
|
+
const result = { name, ...inspected, contents_exposed: false };
|
|
541
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
542
|
+
else console.log(`${name}: available (${inspected.mode || "n/a"}, ${inspected.size} bytes)`);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
throw new Error(`Unknown resource action: ${action}`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function jobCommand(args) {
|
|
550
|
+
const action = String(args._[0] || "list").toLowerCase();
|
|
551
|
+
const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
|
|
552
|
+
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
553
|
+
const manager = new ManagedJobManager({
|
|
554
|
+
jobRoot: join(state.paths.profileDir, "jobs"),
|
|
555
|
+
workspace,
|
|
556
|
+
policy: resolvePolicy({}, state.policy),
|
|
557
|
+
resources: state.resources,
|
|
558
|
+
resourceStatePath: state.paths.statePath,
|
|
559
|
+
logger: createLogger({ level: "warn", component: "job" }),
|
|
560
|
+
});
|
|
561
|
+
let result;
|
|
562
|
+
if (action === "list") result = manager.list({ limit: 50 });
|
|
563
|
+
else if (action === "read") result = manager.read({ job_id: args._[1] });
|
|
564
|
+
else if (action === "inspect") result = manager.inspectLocal({ job_id: args._[1] });
|
|
565
|
+
else if (action === "cancel") result = manager.cancel({ job_id: args._[1] });
|
|
566
|
+
else if (action === "approve") {
|
|
567
|
+
if (args.json && !args.yes) throw new Error("job approve --json requires --yes");
|
|
568
|
+
const inspection = manager.inspectLocal({ job_id: args._[1] });
|
|
569
|
+
if (!args.yes) {
|
|
570
|
+
console.log(JSON.stringify(inspection, null, 2));
|
|
571
|
+
const approved = await confirm(`Approve and execute managed job ${args._[1]}?`, false);
|
|
572
|
+
if (!approved) {
|
|
573
|
+
console.log("Managed job approval cancelled. Re-run with --yes after review to skip confirmation.");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
result = manager.approve({ job_id: args._[1] }, { localOperator: true });
|
|
578
|
+
}
|
|
579
|
+
else if (action === "submit") {
|
|
580
|
+
const planPath = args._[1];
|
|
581
|
+
if (!planPath) throw new Error("job submit requires a JSON plan file");
|
|
582
|
+
const plan = loadManagedJobPlan(expandHome(planPath));
|
|
583
|
+
result = manager.start(plan);
|
|
584
|
+
} else throw new Error(`Unknown job action: ${action}`);
|
|
585
|
+
console.log(JSON.stringify(result, null, 2));
|
|
401
586
|
}
|
|
402
587
|
|
|
403
588
|
async function clientConfigCommand(args) {
|
|
@@ -426,17 +611,13 @@ async function clientConfigCommand(args) {
|
|
|
426
611
|
}
|
|
427
612
|
}
|
|
428
613
|
|
|
429
|
-
function assertNoRemovedLocalApiOptions(args) {
|
|
430
|
-
const removed = ["api", "noApi", "apiPort", "apiHost", "apiKey", "rotateApiKey", "apiModel", "port"].filter((key) => args[key] !== undefined);
|
|
431
|
-
if (removed.length) throw removedLocalApiError();
|
|
432
|
-
}
|
|
433
|
-
|
|
434
614
|
function removedLocalApiError() {
|
|
435
615
|
return new Error("Local /v1 API support has been removed. Use the printed Remote MCP Server URL/password with an MCP client instead.");
|
|
436
616
|
}
|
|
437
617
|
|
|
618
|
+
|
|
438
619
|
async function ensureWorker(state, args) {
|
|
439
|
-
const logger = createLogger({
|
|
620
|
+
const logger = createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "worker" });
|
|
440
621
|
const desiredHash = workerDeployHash(state);
|
|
441
622
|
const expectedVersion = currentPackageVersion();
|
|
442
623
|
const complete = state.worker.url && state.worker.mcpServerUrl && state.worker.oauthPassword && state.worker.daemonSecret && state.worker.oauthTokenVersion && state.worker.name;
|
|
@@ -487,7 +668,7 @@ async function withSecretsFile(state, callback) {
|
|
|
487
668
|
const dir = state.paths.profileDir;
|
|
488
669
|
ensureOwnerOnlyDir(dir);
|
|
489
670
|
cleanupStaleSecretFiles(dir);
|
|
490
|
-
const tempPath = resolve(dir, `worker-secrets-${process.pid}-${Date.now()}.json`);
|
|
671
|
+
const tempPath = resolve(dir, `worker-secrets-${process.pid}-${Date.now()}-${randomBytes(6).toString("hex")}.json`);
|
|
491
672
|
const payload = {
|
|
492
673
|
MCP_OAUTH_PASSWORD: state.worker.oauthPassword,
|
|
493
674
|
DAEMON_SHARED_SECRET: state.worker.daemonSecret,
|
|
@@ -504,10 +685,14 @@ async function withSecretsFile(state, callback) {
|
|
|
504
685
|
|
|
505
686
|
function cleanupStaleSecretFiles(dir) {
|
|
506
687
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
507
|
-
if (!entry.isFile()
|
|
688
|
+
if (!entry.isFile()) continue;
|
|
689
|
+
const match = /^worker-secrets-(\d+)-(\d+)(?:-[a-f0-9]+)?\.json$/.exec(entry.name);
|
|
690
|
+
if (!match) continue;
|
|
508
691
|
const file = resolve(dir, entry.name);
|
|
509
692
|
try {
|
|
510
|
-
|
|
693
|
+
const pid = Number(match[1]);
|
|
694
|
+
const ageMs = Date.now() - statSync(file).mtimeMs;
|
|
695
|
+
if (!isPidAlive(pid) || ageMs > 60 * 60 * 1000) unlinkSync(file);
|
|
511
696
|
} catch {}
|
|
512
697
|
}
|
|
513
698
|
}
|
|
@@ -562,10 +747,18 @@ async function workerHealth(workerUrl, expectedVersion = currentPackageVersion()
|
|
|
562
747
|
if (body?.version !== expectedVersion) return { ok: false, error: `version_mismatch:${body?.version || "unknown"}!=${expectedVersion}` };
|
|
563
748
|
return { ok: true, version: body.version };
|
|
564
749
|
} catch (error) {
|
|
565
|
-
return { ok: false, error: error
|
|
750
|
+
return { ok: false, error: workerHealthError(error) };
|
|
566
751
|
}
|
|
567
752
|
}
|
|
568
753
|
|
|
754
|
+
function workerHealthError(error) {
|
|
755
|
+
const message = String(error?.message || error || "");
|
|
756
|
+
if (/timeout|aborted/i.test(message)) return "timeout";
|
|
757
|
+
if (/certificate|TLS|SSL/i.test(message)) return "tls_error";
|
|
758
|
+
if (/fetch failed|network|ECONN|ENOTFOUND|EAI_AGAIN/i.test(message)) return "network_error";
|
|
759
|
+
return "request_failed";
|
|
760
|
+
}
|
|
761
|
+
|
|
569
762
|
async function retryHealth(workerUrl, expectedVersion, attempts) {
|
|
570
763
|
let last = { ok: false, error: "not_checked" };
|
|
571
764
|
for (let i = 0; i < attempts; i += 1) {
|
|
@@ -583,23 +776,22 @@ function extractWorkerUrl(text = "") {
|
|
|
583
776
|
return anyHttps.find(match => /workers\.dev|\/healthz|\/mcp/.test(match[0]))?.[0]?.replace(/[),.]+$/, "") || "";
|
|
584
777
|
}
|
|
585
778
|
|
|
586
|
-
async function waitForConnectWithNotice(promise, timeoutMs,
|
|
779
|
+
async function waitForConnectWithNotice(promise, timeoutMs, logger) {
|
|
587
780
|
let timeout;
|
|
588
781
|
const timed = new Promise(resolvePromise => {
|
|
589
782
|
timeout = setTimeout(() => resolvePromise("timeout"), timeoutMs);
|
|
590
783
|
});
|
|
591
784
|
const result = await Promise.race([promise.then(() => "connected"), timed]);
|
|
592
785
|
clearTimeout(timeout);
|
|
593
|
-
if (result === "timeout")
|
|
786
|
+
if (result === "timeout") logger.warn("Still connecting; the process will keep retrying");
|
|
594
787
|
}
|
|
595
788
|
|
|
596
789
|
|
|
597
|
-
function printStartJson(state, {
|
|
598
|
-
const mcpPassword = noPrintCredentials ? previewSecret(state.worker.oauthPassword) : state.worker.oauthPassword;
|
|
790
|
+
function printStartJson(state, { showCredentials = false, requestedChangesApplied = true, notice = "" } = {}) {
|
|
599
791
|
createLogger({ component: "ready" }).json({
|
|
600
792
|
mcp: {
|
|
601
793
|
server_url: state.worker.mcpServerUrl,
|
|
602
|
-
connection_password:
|
|
794
|
+
connection_password: showCredentials ? state.worker.oauthPassword : previewSecret(state.worker.oauthPassword),
|
|
603
795
|
worker_url: state.worker.url,
|
|
604
796
|
worker_name: state.worker.name,
|
|
605
797
|
},
|
|
@@ -611,36 +803,45 @@ function printStartJson(state, { noPrintCredentials = false, requestedChangesApp
|
|
|
611
803
|
});
|
|
612
804
|
}
|
|
613
805
|
|
|
614
|
-
function printMcpConnection(state, {
|
|
615
|
-
|
|
806
|
+
function printMcpConnection(state, {
|
|
807
|
+
noPrintCredentials = false,
|
|
808
|
+
includeCredentials = false,
|
|
809
|
+
quiet = false,
|
|
810
|
+
verbose = false,
|
|
811
|
+
policyMigrated = false,
|
|
812
|
+
} = {}) {
|
|
813
|
+
const logger = createLogger({ component: "ready", quiet, level: quiet ? "error" : verbose ? "debug" : "info" });
|
|
616
814
|
const payload = {
|
|
617
815
|
mcp_server_url: state.worker.mcpServerUrl,
|
|
618
816
|
mcp_connection_password: state.worker.oauthPassword,
|
|
619
|
-
worker_url: state.worker.url,
|
|
620
|
-
worker_name: state.worker.name,
|
|
621
817
|
workspace: state.workspace.path,
|
|
622
818
|
state_path: state.paths.statePath,
|
|
623
819
|
policy: state.policy,
|
|
624
820
|
};
|
|
625
|
-
if (json) {
|
|
626
|
-
const safePayload = noPrintCredentials ? { ...payload, mcp_connection_password: previewSecret(payload.mcp_connection_password) } : payload;
|
|
627
|
-
logger.json(safePayload);
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
821
|
if (includeCredentials) {
|
|
631
822
|
logger.success("Remote MCP bridge is ready; save these connection details if your ChatGPT app needs to reconnect");
|
|
632
823
|
logger.plain(` MCP Server URL: ${payload.mcp_server_url}`);
|
|
633
824
|
if (!noPrintCredentials) logger.plain(` MCP connection password: ${payload.mcp_connection_password}`);
|
|
634
825
|
else logger.plain(` MCP connection password: ${previewSecret(payload.mcp_connection_password)} (redacted)`);
|
|
635
826
|
} else {
|
|
636
|
-
logger.success("Remote MCP bridge is ready
|
|
637
|
-
logger.plain("
|
|
827
|
+
logger.success("Remote MCP bridge is ready");
|
|
828
|
+
logger.plain(" Connection credentials unchanged; use --print-mcp-credentials only when reconnecting a client.");
|
|
829
|
+
}
|
|
830
|
+
if (policyMigrated) {
|
|
831
|
+
logger.warn("Legacy implicit policy migrated to full access; use --profile agent, edit, or review to narrow it.");
|
|
638
832
|
}
|
|
639
|
-
logger.plain(` Workspace
|
|
640
|
-
logger.plain(` Policy:
|
|
641
|
-
logger.plain(` State: ${payload.state_path}`);
|
|
833
|
+
logger.plain(` Workspace: ${payload.workspace}`);
|
|
834
|
+
logger.plain(` Policy: ${formatPolicySummary(payload.policy)}`);
|
|
835
|
+
if (verbose) logger.plain(` State: ${payload.state_path}`);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function formatPolicySummary(policy = {}) {
|
|
839
|
+
const scope = policy.unrestrictedPaths ? "all local paths" : "workspace only";
|
|
840
|
+
const environment = policy.minimalEnv ? "isolated env" : "full parent env";
|
|
841
|
+
return `${policy.profile || "custom"} [${policy.origin || "unknown"}; write=${policy.allowWrite ? "on" : "off"}; exec=${policy.execMode || "off"}; ${scope}; ${environment}; absolute_paths=${policy.exposeAbsolutePaths ? "on" : "off"}]`;
|
|
642
842
|
}
|
|
643
843
|
|
|
844
|
+
|
|
644
845
|
function keepProcessAlive({ daemon = null, lock = null, logger = createLogger({ component: "cli" }) } = {}) {
|
|
645
846
|
let stopping = false;
|
|
646
847
|
const stop = async () => {
|
|
@@ -660,8 +861,14 @@ function keepProcessAlive({ daemon = null, lock = null, logger = createLogger({
|
|
|
660
861
|
async function statusCommand(args) {
|
|
661
862
|
const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
|
|
662
863
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
864
|
+
const storedPolicyOrigin = state.policy?.origin;
|
|
865
|
+
state.policy = resolvePolicy({}, state.policy);
|
|
663
866
|
const health = state.worker?.url ? await workerHealth(state.worker.url) : { ok: false, error: "no worker url" };
|
|
664
|
-
const payload = {
|
|
867
|
+
const payload = {
|
|
868
|
+
...redactState(state),
|
|
869
|
+
policyMigrationPending: !storedPolicyOrigin && state.policy.origin === "migrated",
|
|
870
|
+
workerHealth: health,
|
|
871
|
+
};
|
|
665
872
|
console.log(JSON.stringify(payload, null, 2));
|
|
666
873
|
}
|
|
667
874
|
|
|
@@ -674,9 +881,40 @@ async function doctorCommand(args) {
|
|
|
674
881
|
const whoami = await runWrangler(["whoami"], { capture: true, allowFailure: true });
|
|
675
882
|
checks.push({ name: "cloudflare-login", ok: whoami.code === 0, detail: whoami.code === 0 ? "authenticated" : sanitizeLines(whoami.stderr || whoami.stdout) });
|
|
676
883
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
884
|
+
const storedPolicyOrigin = state.policy?.origin;
|
|
885
|
+
state.policy = resolvePolicy({}, state.policy);
|
|
886
|
+
checks.push({ name: "policy", ok: true, detail: formatPolicySummary(state.policy) });
|
|
677
887
|
const health = state.worker?.url ? await workerHealth(state.worker.url) : { ok: false, error: "no worker url" };
|
|
678
888
|
checks.push({ name: "worker-health", ok: health.ok, detail: health.ok ? state.worker.url : health.error });
|
|
679
|
-
|
|
889
|
+
const diagnosticRuntime = new LocalDaemon({
|
|
890
|
+
workspace,
|
|
891
|
+
policy: state.policy,
|
|
892
|
+
logger: createLogger({ level: "error", component: "doctor" }),
|
|
893
|
+
jobRoot: join(state.paths.profileDir, "jobs"),
|
|
894
|
+
resources: state.resources,
|
|
895
|
+
resourceStatePath: state.paths.statePath,
|
|
896
|
+
recoverJobs: false,
|
|
897
|
+
});
|
|
898
|
+
let runtimeDiagnostics;
|
|
899
|
+
try {
|
|
900
|
+
runtimeDiagnostics = await diagnosticRuntime.diagnoseRuntime();
|
|
901
|
+
} finally {
|
|
902
|
+
diagnosticRuntime.stop();
|
|
903
|
+
}
|
|
904
|
+
for (const check of runtimeDiagnostics.checks) {
|
|
905
|
+
checks.push({
|
|
906
|
+
name: `runtime:${check.layer}`,
|
|
907
|
+
ok: check.skipped === true || check.ok === true,
|
|
908
|
+
detail: check.skipped ? `skipped (${check.error_class || "not applicable"})` : check.ok ? "ok" : check.error_class || "failed",
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
console.log(JSON.stringify({
|
|
912
|
+
ok: checks.every(check => check.ok),
|
|
913
|
+
checks,
|
|
914
|
+
runtimeDiagnostics,
|
|
915
|
+
policyMigrationPending: !storedPolicyOrigin && state.policy.origin === "migrated",
|
|
916
|
+
state: redactState(state),
|
|
917
|
+
}, null, 2));
|
|
680
918
|
}
|
|
681
919
|
|
|
682
920
|
async function rotateSecretsCommand(args) {
|
|
@@ -688,7 +926,7 @@ async function rotateSecretsCommand(args) {
|
|
|
688
926
|
throw new Error(`another startup/deployment operation is already running for this workspace (${pid})`);
|
|
689
927
|
}
|
|
690
928
|
try {
|
|
691
|
-
await stopAutostartBestEffort(
|
|
929
|
+
await stopAutostartBestEffort(createLogger({ level: args.quiet ? "error" : "warn", component: "service" }));
|
|
692
930
|
await sleep(500);
|
|
693
931
|
const daemonOwner = readDaemonLockOwner(daemonLockPathForState(state));
|
|
694
932
|
if (daemonOwner?.pid && isPidAlive(daemonOwner.pid)) {
|
|
@@ -696,9 +934,10 @@ async function rotateSecretsCommand(args) {
|
|
|
696
934
|
}
|
|
697
935
|
ensureWorkerSecrets(state, { rotateSecrets: true, workerName: validateWorkerName(args.workerName) });
|
|
698
936
|
saveState(state);
|
|
699
|
-
|
|
700
|
-
console.log(`Rotated
|
|
701
|
-
console.log(
|
|
937
|
+
const showMcpPassword = Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials);
|
|
938
|
+
console.log(`Rotated MCP connection password: ${showMcpPassword ? state.worker.oauthPassword : previewSecret(state.worker.oauthPassword)}`);
|
|
939
|
+
console.log(`Rotated daemon secret: ${previewSecret(state.worker.daemonSecret)}`);
|
|
940
|
+
console.log("Run `machine-mcp --print-mcp-credentials` to redeploy and display the new client connection credentials when needed.");
|
|
702
941
|
} finally {
|
|
703
942
|
startupLock.release();
|
|
704
943
|
}
|
|
@@ -742,23 +981,23 @@ async function serviceCommand(args) {
|
|
|
742
981
|
throw new Error(`Unknown service action: ${action}`);
|
|
743
982
|
}
|
|
744
983
|
|
|
745
|
-
async function installAutostartBestEffort({ workspace, stateRoot, entryScript }) {
|
|
984
|
+
async function installAutostartBestEffort({ workspace, stateRoot, entryScript, logger }) {
|
|
746
985
|
try {
|
|
747
986
|
const { installAutostart } = await import("./service.mjs");
|
|
748
|
-
const result = await installAutostart({ workspace, stateRoot, entryScript, logger: structuredLogger(
|
|
749
|
-
if (result?.ok)
|
|
750
|
-
else
|
|
987
|
+
const result = await installAutostart({ workspace, stateRoot, entryScript, logger: structuredLogger(true) });
|
|
988
|
+
if (result?.ok) logger.info("Autostart installed for future logins", { provider: result.provider });
|
|
989
|
+
else logger.warn("Autostart installation reported a problem; run `machine-mcp service status` for details");
|
|
751
990
|
} catch (error) {
|
|
752
|
-
|
|
991
|
+
logger.warn("Autostart installation skipped", { error_class: classifyOperationalError(error) });
|
|
753
992
|
}
|
|
754
993
|
}
|
|
755
994
|
|
|
756
|
-
async function stopAutostartBestEffort(
|
|
995
|
+
async function stopAutostartBestEffort(logger) {
|
|
757
996
|
try {
|
|
758
997
|
const { stopAutostart } = await import("./service.mjs");
|
|
759
998
|
await stopAutostart({ logger: structuredLogger(true) });
|
|
760
999
|
} catch (error) {
|
|
761
|
-
|
|
1000
|
+
logger.warn("Autostart stop skipped", { error_class: classifyOperationalError(error) });
|
|
762
1001
|
}
|
|
763
1002
|
}
|
|
764
1003
|
|
|
@@ -774,6 +1013,12 @@ async function uninstallCommand(args) {
|
|
|
774
1013
|
console.log("Uninstall cancelled. Re-run with `machine-mcp uninstall --yes` to skip confirmation.");
|
|
775
1014
|
return;
|
|
776
1015
|
}
|
|
1016
|
+
const activeJobs = activeStateJobs(stateRoot);
|
|
1017
|
+
if (activeJobs.length) {
|
|
1018
|
+
const detail = activeJobs.slice(0, 5).map((item) => `${item.job_id}:${item.status}`).join(", ");
|
|
1019
|
+
const suffix = activeJobs.length > 5 ? `, and ${activeJobs.length - 5} more` : "";
|
|
1020
|
+
throw new Error(`refusing to uninstall while managed jobs are active (${detail}${suffix}); inspect or cancel them with machine-mcp job list/cancel`);
|
|
1021
|
+
}
|
|
777
1022
|
const autostartRemoved = await removeAutostartBestEffort(stateRoot);
|
|
778
1023
|
if (!autostartRemoved) throw new Error("autostart removal failed; state and Worker were kept so the uninstall can be retried safely");
|
|
779
1024
|
await sleep(500);
|
|
@@ -820,6 +1065,7 @@ function knownWorkerNames(stateRoot) {
|
|
|
820
1065
|
const stateFile = resolve(profiles, entry.name, "state.json");
|
|
821
1066
|
if (!existsSync(stateFile)) continue;
|
|
822
1067
|
try {
|
|
1068
|
+
if (statSync(stateFile).size > 2 * 1024 * 1024) continue;
|
|
823
1069
|
const state = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
824
1070
|
const name = String(state?.worker?.name || "");
|
|
825
1071
|
if (/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(name)) names.add(name);
|
|
@@ -839,11 +1085,24 @@ async function removeAutostartBestEffort(stateRoot) {
|
|
|
839
1085
|
}
|
|
840
1086
|
return true;
|
|
841
1087
|
} catch (error) {
|
|
842
|
-
console.warn(`Autostart removal skipped or failed
|
|
1088
|
+
console.warn(`Autostart removal skipped or failed (${classifyOperationalError(error)}). Run machine-mcp service status for details.`);
|
|
843
1089
|
return false;
|
|
844
1090
|
}
|
|
845
1091
|
}
|
|
846
1092
|
|
|
1093
|
+
function activeStateJobs(stateRoot) {
|
|
1094
|
+
const profiles = resolve(expandHome(stateRoot), "profiles");
|
|
1095
|
+
if (!existsSync(profiles)) return [];
|
|
1096
|
+
const active = [];
|
|
1097
|
+
for (const profile of readdirSync(profiles, { withFileTypes: true })) {
|
|
1098
|
+
if (!profile.isDirectory()) continue;
|
|
1099
|
+
for (const job of activeManagedJobs(resolve(profiles, profile.name, "jobs"))) {
|
|
1100
|
+
active.push({ profile: profile.name, ...job });
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return active;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
847
1106
|
function activeStateLocks(stateRoot) {
|
|
848
1107
|
const profiles = resolve(expandHome(stateRoot), "profiles");
|
|
849
1108
|
if (!existsSync(profiles)) return [];
|
|
@@ -872,9 +1131,10 @@ function isPidAlive(pid) {
|
|
|
872
1131
|
}
|
|
873
1132
|
|
|
874
1133
|
function structuredLogger(quiet) {
|
|
875
|
-
return createLogger({ quiet, component: "
|
|
1134
|
+
return createLogger({ quiet, component: "service" });
|
|
876
1135
|
}
|
|
877
1136
|
|
|
1137
|
+
|
|
878
1138
|
function sanitizeLines(text) {
|
|
879
1139
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
880
1140
|
const homePattern = home ? new RegExp(escapeRegExp(home), "g") : null;
|
|
@@ -910,7 +1170,7 @@ function usage() {
|
|
|
910
1170
|
console.log(`machine-bridge-mcp
|
|
911
1171
|
|
|
912
1172
|
Usage:
|
|
913
|
-
npm install -g machine-bridge-mcp@latest && machine-mcp
|
|
1173
|
+
npm install -g --allow-scripts=esbuild,workerd,sharp machine-bridge-mcp@latest && machine-mcp
|
|
914
1174
|
npx machine-bridge-mcp@latest # no global install; autostart may rely on npm cache
|
|
915
1175
|
./mbm # from source checkout
|
|
916
1176
|
.\\mbm.cmd # from source checkout on Windows cmd
|
|
@@ -946,9 +1206,14 @@ Start options:
|
|
|
946
1206
|
--no-exec Disable run_process and exec_command
|
|
947
1207
|
--full-env Pass the full parent environment to local commands
|
|
948
1208
|
--unrestricted-paths Allow filesystem tools outside the workspace
|
|
949
|
-
--absolute-paths Return absolute local paths
|
|
1209
|
+
--absolute-paths Return absolute local paths (enabled by the full profile)
|
|
950
1210
|
--state-dir DIR Override state root
|
|
951
|
-
--json Print
|
|
1211
|
+
--json Print connection details as JSON; credentials stay redacted unless explicitly requested
|
|
1212
|
+
--log-level LEVEL error, warn, info (default), or debug
|
|
1213
|
+
--verbose Alias for --log-level debug; includes per-tool success/correlation logs
|
|
1214
|
+
--quiet Alias for --log-level error
|
|
1215
|
+
--allow-insecure-permissions
|
|
1216
|
+
Permit resource registration when a file is group/other-readable
|
|
952
1217
|
|
|
953
1218
|
Uninstall options:
|
|
954
1219
|
--keep-worker Do not delete deployed Worker(s) during uninstall
|