machine-bridge-mcp 0.5.0 → 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 +27 -0
- package/README.md +93 -5
- package/SECURITY.md +33 -3
- package/docs/ARCHITECTURE.md +41 -8
- package/docs/CLIENTS.md +13 -2
- package/docs/LOGGING.md +3 -1
- package/docs/MANAGED_JOBS.md +274 -0
- package/docs/OPERATIONS.md +55 -4
- package/docs/TESTING.md +6 -1
- package/package.json +5 -4
- package/src/local/cli.mjs +192 -5
- package/src/local/daemon.mjs +101 -1
- package/src/local/job-runner.mjs +470 -0
- package/src/local/managed-jobs.mjs +854 -0
- package/src/local/state.mjs +51 -8
- package/src/local/stdio.mjs +2 -2
- package/src/shared/server-metadata.json +8 -1
- package/src/shared/tool-catalog.json +498 -0
- package/src/worker/index.ts +1 -1
package/src/local/cli.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
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
8
|
import { DEFAULT_POLICY_PROFILE, DEFAULT_POLICY_REVISION, POLICY_PROFILES, normalizePolicy, policyProfile } from "./tools.mjs";
|
|
9
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,7 +38,7 @@ 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
44
|
"workspace", "stateDir", "workerName", "profile", "execMode", "client", "logLevel",
|
|
@@ -63,6 +64,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
63
64
|
case "service":
|
|
64
65
|
case "autostart": return serviceCommand(args);
|
|
65
66
|
case "rotate-secrets": return rotateSecretsCommand(args);
|
|
67
|
+
case "resource": return resourceCommand(args);
|
|
68
|
+
case "job": return jobCommand(args);
|
|
66
69
|
case "uninstall": return uninstallCommand(args);
|
|
67
70
|
default:
|
|
68
71
|
console.error(`Unknown command: ${command}`);
|
|
@@ -85,6 +88,8 @@ const COMMAND_OPTIONS = {
|
|
|
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
|
|
|
@@ -118,8 +123,6 @@ function toKebab(value) {
|
|
|
118
123
|
|
|
119
124
|
export function validatePositionals(command, args) {
|
|
120
125
|
const count = args._.length;
|
|
121
|
-
const conflict = Boolean(args.workspace) && count > (command === "workspace" || command === "service" || command === "autostart" ? 1 : 0);
|
|
122
|
-
if (conflict) throw new Error("workspace path was provided both positionally and with --workspace");
|
|
123
126
|
if (["start", "stdio", "status", "doctor", "rotate-secrets"].includes(command)) {
|
|
124
127
|
if (count > 1) throw new Error(`${command} accepts at most one positional workspace path`);
|
|
125
128
|
if (args.workspace && count) throw new Error("workspace path was provided both positionally and with --workspace");
|
|
@@ -143,6 +146,18 @@ export function validatePositionals(command, args) {
|
|
|
143
146
|
if (count > 1) throw new Error("client-config accepts at most one positional client name");
|
|
144
147
|
return;
|
|
145
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
|
+
}
|
|
146
161
|
if (command === "uninstall" && count) throw new Error("uninstall does not accept positional arguments");
|
|
147
162
|
}
|
|
148
163
|
|
|
@@ -336,6 +351,9 @@ async function startCommand(args) {
|
|
|
336
351
|
workspace,
|
|
337
352
|
policy: state.policy,
|
|
338
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,
|
|
339
357
|
onSuperseded: () => {
|
|
340
358
|
logger.warn("this daemon was replaced by a newer authenticated instance; exiting without reconnecting");
|
|
341
359
|
lock.release();
|
|
@@ -439,7 +457,132 @@ async function stdioCommand(args) {
|
|
|
439
457
|
const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
|
|
440
458
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
441
459
|
const policy = resolvePolicy(args, state.policy);
|
|
442
|
-
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));
|
|
443
586
|
}
|
|
444
587
|
|
|
445
588
|
async function clientConfigCommand(args) {
|
|
@@ -743,9 +886,32 @@ async function doctorCommand(args) {
|
|
|
743
886
|
checks.push({ name: "policy", ok: true, detail: formatPolicySummary(state.policy) });
|
|
744
887
|
const health = state.worker?.url ? await workerHealth(state.worker.url) : { ok: false, error: "no worker url" };
|
|
745
888
|
checks.push({ name: "worker-health", ok: health.ok, detail: health.ok ? state.worker.url : health.error });
|
|
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
|
+
}
|
|
746
911
|
console.log(JSON.stringify({
|
|
747
912
|
ok: checks.every(check => check.ok),
|
|
748
913
|
checks,
|
|
914
|
+
runtimeDiagnostics,
|
|
749
915
|
policyMigrationPending: !storedPolicyOrigin && state.policy.origin === "migrated",
|
|
750
916
|
state: redactState(state),
|
|
751
917
|
}, null, 2));
|
|
@@ -847,6 +1013,12 @@ async function uninstallCommand(args) {
|
|
|
847
1013
|
console.log("Uninstall cancelled. Re-run with `machine-mcp uninstall --yes` to skip confirmation.");
|
|
848
1014
|
return;
|
|
849
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
|
+
}
|
|
850
1022
|
const autostartRemoved = await removeAutostartBestEffort(stateRoot);
|
|
851
1023
|
if (!autostartRemoved) throw new Error("autostart removal failed; state and Worker were kept so the uninstall can be retried safely");
|
|
852
1024
|
await sleep(500);
|
|
@@ -918,6 +1090,19 @@ async function removeAutostartBestEffort(stateRoot) {
|
|
|
918
1090
|
}
|
|
919
1091
|
}
|
|
920
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
|
+
|
|
921
1106
|
function activeStateLocks(stateRoot) {
|
|
922
1107
|
const profiles = resolve(expandHome(stateRoot), "profiles");
|
|
923
1108
|
if (!existsSync(profiles)) return [];
|
|
@@ -1027,6 +1212,8 @@ Start options:
|
|
|
1027
1212
|
--log-level LEVEL error, warn, info (default), or debug
|
|
1028
1213
|
--verbose Alias for --log-level debug; includes per-tool success/correlation logs
|
|
1029
1214
|
--quiet Alias for --log-level error
|
|
1215
|
+
--allow-insecure-permissions
|
|
1216
|
+
Permit resource registration when a file is group/other-readable
|
|
1030
1217
|
|
|
1031
1218
|
Uninstall options:
|
|
1032
1219
|
--keep-worker Do not delete deployed Worker(s) during uninstall
|
package/src/local/daemon.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { MAX_COMMAND_BYTES, ProcessSessionManager, terminateProcessTree, validat
|
|
|
11
11
|
export { MAX_COMMAND_BYTES } from "./process-sessions.mjs";
|
|
12
12
|
import { MCP_PROTOCOL_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS, normalizePolicy, SERVER_NAME, toolNamesForPolicy } from "./tools.mjs";
|
|
13
13
|
import { classifyOperationalError } from "./log.mjs";
|
|
14
|
+
import { ManagedJobManager } from "./managed-jobs.mjs";
|
|
14
15
|
|
|
15
16
|
export const MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
16
17
|
const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
@@ -22,7 +23,7 @@ const MAX_IMAGE_BYTES = 4 * 1024 * 1024;
|
|
|
22
23
|
const SLOW_TOOL_CALL_MS = 30_000;
|
|
23
24
|
|
|
24
25
|
export class LocalDaemon {
|
|
25
|
-
constructor({ workerUrl = "", secret = "", workspace, policy, logger = console, onSuperseded = null }) {
|
|
26
|
+
constructor({ workerUrl = "", secret = "", workspace, policy, logger = console, onSuperseded = null, jobRoot = "", resources = {}, resourceStatePath = "", recoverJobs = true }) {
|
|
26
27
|
this.workerUrl = workerUrl ? normalizeWorkerUrl(workerUrl) : "";
|
|
27
28
|
if (this.workerUrl && (typeof secret !== "string" || secret.length < 16)) throw new Error("daemon secret is missing or too short");
|
|
28
29
|
this.secret = secret || "";
|
|
@@ -46,6 +47,16 @@ export class LocalDaemon {
|
|
|
46
47
|
this.reconnectAttempt = 0;
|
|
47
48
|
this.mutationQueue = Promise.resolve();
|
|
48
49
|
this.runtimeDir = createRuntimeDir();
|
|
50
|
+
if (typeof jobRoot !== "string" || !jobRoot.trim()) throw new Error("persistent managed-job root is required");
|
|
51
|
+
this.managedJobManager = new ManagedJobManager({
|
|
52
|
+
jobRoot,
|
|
53
|
+
workspace: this.workspace,
|
|
54
|
+
policy: this.policy,
|
|
55
|
+
resources,
|
|
56
|
+
resourceStatePath,
|
|
57
|
+
logger: this.logger,
|
|
58
|
+
recover: recoverJobs,
|
|
59
|
+
});
|
|
49
60
|
this.processSessionManager = new ProcessSessionManager({
|
|
50
61
|
workspace: this.workspace,
|
|
51
62
|
policy: this.policy,
|
|
@@ -85,6 +96,8 @@ export class LocalDaemon {
|
|
|
85
96
|
environment: this.policy.minimalEnv ? "isolated-minimal" : "full-parent",
|
|
86
97
|
runtime_dir: this.policy.exposeAbsolutePaths ? this.runtimeDir : "<private-runtime-dir>",
|
|
87
98
|
process_sessions: this.processSessionManager.status(),
|
|
99
|
+
managed_jobs: this.managedJobManager.status(),
|
|
100
|
+
local_resources: this.managedJobManager.resourceInfo(),
|
|
88
101
|
},
|
|
89
102
|
};
|
|
90
103
|
}
|
|
@@ -291,6 +304,13 @@ export class LocalDaemon {
|
|
|
291
304
|
case "git_diff": return this.gitDiff(args, context);
|
|
292
305
|
case "git_log": return this.gitLog(args, context);
|
|
293
306
|
case "git_show": return this.gitShow(args, context);
|
|
307
|
+
case "diagnose_runtime": return this.diagnoseRuntime(context);
|
|
308
|
+
case "list_local_resources": return this.managedJobManager.listResources();
|
|
309
|
+
case "stage_job": return this.managedJobManager.stage(args);
|
|
310
|
+
case "start_job": return this.managedJobManager.start(args);
|
|
311
|
+
case "list_jobs": return this.managedJobManager.list(args);
|
|
312
|
+
case "read_job": return this.managedJobManager.read(args);
|
|
313
|
+
case "cancel_job": return this.managedJobManager.cancel(args);
|
|
294
314
|
case "run_process": return this.runDirectProcess(args, context);
|
|
295
315
|
case "start_process": return this.processSessionManager.start(args, context);
|
|
296
316
|
case "read_process": return this.processSessionManager.read(args, context);
|
|
@@ -629,6 +649,86 @@ export class LocalDaemon {
|
|
|
629
649
|
return { ok: true, target, root, pathspec: repoRelative || "" };
|
|
630
650
|
}
|
|
631
651
|
|
|
652
|
+
async diagnoseRuntime(context = {}) {
|
|
653
|
+
this.throwIfCancelled(context);
|
|
654
|
+
const checks = [];
|
|
655
|
+
checks.push({
|
|
656
|
+
layer: "mcp-host-to-daemon",
|
|
657
|
+
ok: true,
|
|
658
|
+
detail: "This diagnostic request reached the local Machine Bridge runtime.",
|
|
659
|
+
});
|
|
660
|
+
checks.push({
|
|
661
|
+
layer: "machine-bridge-policy",
|
|
662
|
+
ok: this.policy.execMode === "direct" || this.policy.execMode === "shell",
|
|
663
|
+
detail: `profile=${this.policy.profile}; exec_mode=${this.policy.execMode}; unrestricted_paths=${this.policy.unrestrictedPaths}`,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
const probe = join(this.runtimeDir, `.diagnostic-${process.pid}-${randomBytes(6).toString("hex")}`);
|
|
667
|
+
try {
|
|
668
|
+
await writeFile(probe, "ok\n", { mode: 0o600, flag: "wx" });
|
|
669
|
+
const { buffer } = await readBoundedFile(probe, 64, "diagnostic file");
|
|
670
|
+
checks.push({ layer: "local-filesystem", ok: buffer.toString("utf8") === "ok\n", error_class: null });
|
|
671
|
+
} catch (error) {
|
|
672
|
+
checks.push({ layer: "local-filesystem", ok: false, error_class: classifyOperationalError(error) });
|
|
673
|
+
} finally {
|
|
674
|
+
await rm(probe, { force: true }).catch(() => {});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (this.policy.execMode === "direct" || this.policy.execMode === "shell") {
|
|
678
|
+
const direct = await this.runProcess(
|
|
679
|
+
process.execPath,
|
|
680
|
+
["-e", "process.stdout.write('ok')"],
|
|
681
|
+
5000,
|
|
682
|
+
true,
|
|
683
|
+
1024,
|
|
684
|
+
context,
|
|
685
|
+
this.workspace,
|
|
686
|
+
).catch((error) => ({ code: 127, stdout: "", stderr: "", error_class: classifyOperationalError(error) }));
|
|
687
|
+
checks.push({
|
|
688
|
+
layer: "local-process-spawn",
|
|
689
|
+
ok: direct.code === 0 && direct.stdout === "ok",
|
|
690
|
+
error_class: direct.error_class || (direct.code === 0 ? null : classifyOperationalError(direct.stderr || direct.stdout || "execution failed")),
|
|
691
|
+
});
|
|
692
|
+
} else {
|
|
693
|
+
checks.push({ layer: "local-process-spawn", ok: false, skipped: true, error_class: "policy_denied" });
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (this.policy.execMode === "shell") {
|
|
697
|
+
const shell = workspaceShellCommand(process.platform === "win32" ? "cd" : "pwd");
|
|
698
|
+
const result = await this.runProcess(shell.cmd, shell.args, 5000, true, 4096, context, this.workspace)
|
|
699
|
+
.catch((error) => ({ code: 127, error_class: classifyOperationalError(error) }));
|
|
700
|
+
checks.push({
|
|
701
|
+
layer: "local-shell",
|
|
702
|
+
ok: result.code === 0,
|
|
703
|
+
error_class: result.error_class || (result.code === 0 ? null : classifyOperationalError(result.stderr || result.stdout || "execution failed")),
|
|
704
|
+
});
|
|
705
|
+
} else {
|
|
706
|
+
checks.push({ layer: "local-shell", ok: false, skipped: true, error_class: "policy_denied" });
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const storage = this.managedJobManager.diagnoseStorage();
|
|
710
|
+
checks.push({ layer: "managed-job-storage", ...storage });
|
|
711
|
+
const resources = this.managedJobManager.listResources();
|
|
712
|
+
checks.push({
|
|
713
|
+
layer: "local-resource-registry",
|
|
714
|
+
ok: resources.resources.every((resource) => resource.available),
|
|
715
|
+
registered: resources.count,
|
|
716
|
+
unavailable: resources.resources.filter((resource) => !resource.available).map((resource) => ({ name: resource.name, error_class: resource.error_class })),
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
request_reached_local_runtime: true,
|
|
721
|
+
interpretation: {
|
|
722
|
+
tool_call_blocked_before_response: "host/platform or connector gateway",
|
|
723
|
+
diagnostic_reached_daemon_but_spawn_failed: "local OS, endpoint security, shell configuration, or Machine Bridge policy",
|
|
724
|
+
managed_job_accepted_then_later_tools_blocked: "job continues independently; inspect with local CLI or a later read_job call",
|
|
725
|
+
},
|
|
726
|
+
policy: this.policy,
|
|
727
|
+
checks,
|
|
728
|
+
ok: checks.filter((check) => !check.skipped).every((check) => check.ok),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
632
732
|
async runDirectProcess(args, context = {}) {
|
|
633
733
|
if (this.policy.execMode !== "direct" && this.policy.execMode !== "shell") throw new Error("run_process is disabled by daemon policy");
|
|
634
734
|
const argv = validateArgv(args.argv);
|