machine-bridge-mcp 0.5.0 → 0.6.2

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/src/local/cli.mjs CHANGED
@@ -1,13 +1,16 @@
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
- import { DEFAULT_POLICY_PROFILE, DEFAULT_POLICY_REVISION, POLICY_PROFILES, normalizePolicy, policyProfile } from "./tools.mjs";
8
+ import { assertCanonicalFullPolicy, DEFAULT_POLICY_PROFILE, DEFAULT_POLICY_REVISION, POLICY_PROFILES, normalizePolicy, policyProfile, toolsForPolicy } 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";
12
+ import { generateRegisteredSshKey } from "./resource-operations.mjs";
13
+ import { runFullAccessTest } from "./full-access-test.mjs";
11
14
  import {
12
15
  acquireDaemonLock,
13
16
  acquireStartupLock,
@@ -37,7 +40,7 @@ const BOOLEAN_OPTIONS = new Set([
37
40
  "help", "version", "quiet", "json", "verbose", "rotateSecrets", "forceWorker",
38
41
  "daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials",
39
42
  "printCredentials", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
40
- "yes", "keepWorker",
43
+ "yes", "keepWorker", "allowInsecurePermissions",
41
44
  ]);
42
45
  const VALUE_OPTIONS = new Set([
43
46
  "workspace", "stateDir", "workerName", "profile", "execMode", "client", "logLevel",
@@ -59,10 +62,13 @@ export async function main(argv = process.argv.slice(2)) {
59
62
  case "client-config": return clientConfigCommand(args);
60
63
  case "status": return statusCommand(args);
61
64
  case "doctor": return doctorCommand(args);
65
+ case "full-test": return fullTestCommand(args);
62
66
  case "workspace": return workspaceCommand(args);
63
67
  case "service":
64
68
  case "autostart": return serviceCommand(args);
65
69
  case "rotate-secrets": return rotateSecretsCommand(args);
70
+ case "resource": return resourceCommand(args);
71
+ case "job": return jobCommand(args);
66
72
  case "uninstall": return uninstallCommand(args);
67
73
  default:
68
74
  console.error(`Unknown command: ${command}`);
@@ -81,10 +87,13 @@ const COMMAND_OPTIONS = {
81
87
  "client-config": new Set(["workspace", "stateDir", "profile", "client", "json"]),
82
88
  status: new Set(["workspace", "stateDir"]),
83
89
  doctor: new Set(["workspace", "stateDir"]),
90
+ "full-test": new Set(["workspace", "stateDir", "json"]),
84
91
  "rotate-secrets": new Set(["workspace", "stateDir", "workerName", "noPrintCredentials", "printMcpCredentials", "printCredentials", "quiet"]),
85
92
  workspace: new Set(["workspace", "stateDir"]),
86
93
  service: new Set(["workspace", "stateDir", "quiet"]),
87
94
  autostart: new Set(["workspace", "stateDir", "quiet"]),
95
+ resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "json"]),
96
+ job: new Set(["workspace", "stateDir", "json", "yes"]),
88
97
  uninstall: new Set(["stateDir", "keepWorker", "yes"]),
89
98
  };
90
99
 
@@ -118,9 +127,7 @@ function toKebab(value) {
118
127
 
119
128
  export function validatePositionals(command, args) {
120
129
  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
- if (["start", "stdio", "status", "doctor", "rotate-secrets"].includes(command)) {
130
+ if (["start", "stdio", "status", "doctor", "full-test", "rotate-secrets"].includes(command)) {
124
131
  if (count > 1) throw new Error(`${command} accepts at most one positional workspace path`);
125
132
  if (args.workspace && count) throw new Error("workspace path was provided both positionally and with --workspace");
126
133
  return;
@@ -143,6 +150,18 @@ export function validatePositionals(command, args) {
143
150
  if (count > 1) throw new Error("client-config accepts at most one positional client name");
144
151
  return;
145
152
  }
153
+ if (command === "resource") {
154
+ const action = String(args._[0] || "list");
155
+ const max = action === "add" || action === "generate-ssh-key" ? 3 : action === "remove" || action === "check" ? 2 : 1;
156
+ if (count > max) throw new Error(`resource ${action} received too many positional arguments`);
157
+ return;
158
+ }
159
+ if (command === "job") {
160
+ const action = String(args._[0] || "list");
161
+ const max = action === "read" || action === "inspect" || action === "cancel" || action === "approve" || action === "submit" ? 2 : 1;
162
+ if (count > max) throw new Error(`job ${action} received too many positional arguments`);
163
+ return;
164
+ }
146
165
  if (command === "uninstall" && count) throw new Error("uninstall does not accept positional arguments");
147
166
  }
148
167
 
@@ -336,6 +355,9 @@ async function startCommand(args) {
336
355
  workspace,
337
356
  policy: state.policy,
338
357
  logger: createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "daemon" }),
358
+ jobRoot: join(state.paths.profileDir, "jobs"),
359
+ resources: state.resources,
360
+ resourceStatePath: state.paths.statePath,
339
361
  onSuperseded: () => {
340
362
  logger.warn("this daemon was replaced by a newer authenticated instance; exiting without reconnecting");
341
363
  lock.release();
@@ -439,7 +461,168 @@ async function stdioCommand(args) {
439
461
  const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
440
462
  const state = loadState(workspace, { stateDir: args.stateDir });
441
463
  const policy = resolvePolicy(args, state.policy);
442
- await runStdioServer({ workspace, policy, logLevel: effectiveLogLevel(args) });
464
+ await runStdioServer({
465
+ workspace,
466
+ policy,
467
+ logLevel: effectiveLogLevel(args),
468
+ jobRoot: join(state.paths.profileDir, "jobs"),
469
+ resources: state.resources,
470
+ resourceStatePath: state.paths.statePath,
471
+ });
472
+ }
473
+
474
+ async function resourceCommand(args) {
475
+ const action = String(args._[0] || "list").toLowerCase();
476
+ const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
477
+ const state = loadState(workspace, { stateDir: args.stateDir });
478
+ state.resources ||= {};
479
+
480
+ if (action === "list") {
481
+ const resources = publicResourceRegistry(state.resources);
482
+ if (args.json) console.log(JSON.stringify({ workspace, resources }, null, 2));
483
+ else if (!Object.keys(resources).length) console.log("No local resources registered.");
484
+ else for (const [name, value] of Object.entries(resources)) console.log(`${name} ${value.path} ${value.mode || "n/a"} ${value.size ?? "n/a"} bytes`);
485
+ return;
486
+ }
487
+
488
+ if (action === "add") {
489
+ const name = validateResourceName(args._[1]);
490
+ const inputPath = args._[2];
491
+ if (!inputPath) throw new Error("resource add requires NAME and FILE_PATH");
492
+ const lock = acquireStartupLock(state);
493
+ if (!lock.acquired) throw new Error("another state-changing operation is already running for this workspace");
494
+ try {
495
+ const latest = loadState(workspace, { stateDir: args.stateDir });
496
+ latest.resources ||= {};
497
+ const inspected = inspectResourceFile(expandHome(inputPath), { allowInsecurePermissions: args.allowInsecurePermissions === true });
498
+ if (!Object.prototype.hasOwnProperty.call(latest.resources, name) && Object.keys(latest.resources).length >= 64) {
499
+ throw new Error("local resource registry limit reached (64)");
500
+ }
501
+ latest.resources[name] = inspected;
502
+ saveState(latest);
503
+ const result = { name, ...inspected, contents_exposed: false, available_to_new_jobs_immediately: true };
504
+ if (args.json) console.log(JSON.stringify(result, null, 2));
505
+ else {
506
+ console.log(`Registered local resource: ${name}`);
507
+ console.log(`Path: ${inspected.path}`);
508
+ console.log(`Mode: ${inspected.mode || "n/a"}; size: ${inspected.size} bytes`);
509
+ console.log("The resource is available to newly submitted managed jobs immediately.");
510
+ }
511
+ } finally {
512
+ lock.release();
513
+ }
514
+ return;
515
+ }
516
+
517
+ if (action === "generate-ssh-key") {
518
+ const name = validateResourceName(args._[1]);
519
+ const home = process.env.HOME || process.env.USERPROFILE;
520
+ if (!home) throw new Error("HOME or USERPROFILE is required to choose a default SSH key path");
521
+ const requestedPath = args._[2] ? expandHome(args._[2]) : join(home, ".ssh", `machine-mcp-${name}-ed25519`);
522
+ const key = await generateRegisteredSshKey({
523
+ workspace,
524
+ stateDir: args.stateDir,
525
+ name,
526
+ targetPath: requestedPath,
527
+ comment: `machine-mcp:${name}`,
528
+ });
529
+ const result = {
530
+ name: key.name,
531
+ created: key.created,
532
+ private_key_path: key.privateKeyPath,
533
+ public_key_path: key.publicKeyPath,
534
+ fingerprint: key.fingerprint,
535
+ key_type: key.keyType,
536
+ private_mode: key.privateMode,
537
+ public_mode: key.publicMode,
538
+ private_key_content_exposed: key.privateKeyContentExposed,
539
+ registered: key.registered,
540
+ available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
541
+ };
542
+ if (args.json) console.log(JSON.stringify(result, null, 2));
543
+ else {
544
+ console.log(`${key.created ? "Generated and registered" : "Reused and registered"} SSH key resource: ${name}`);
545
+ console.log(`Private key: ${key.privateKeyPath}`);
546
+ console.log(`Public key: ${key.publicKeyPath}`);
547
+ console.log(`Fingerprint: ${key.fingerprint}`);
548
+ console.log("Private key content was not printed or sent through MCP.");
549
+ }
550
+ return;
551
+ }
552
+
553
+ if (action === "remove") {
554
+ const name = validateResourceName(args._[1]);
555
+ const lock = acquireStartupLock(state);
556
+ if (!lock.acquired) throw new Error("another state-changing operation is already running for this workspace");
557
+ try {
558
+ const latest = loadState(workspace, { stateDir: args.stateDir });
559
+ latest.resources ||= {};
560
+ const existed = Object.prototype.hasOwnProperty.call(latest.resources, name);
561
+ delete latest.resources[name];
562
+ saveState(latest);
563
+ const result = { name, removed: existed, affects_new_jobs_immediately: true };
564
+ if (args.json) console.log(JSON.stringify(result, null, 2));
565
+ else {
566
+ console.log(existed ? `Removed local resource: ${name}` : `Local resource was not registered: ${name}`);
567
+ console.log("The change applies to newly submitted managed jobs immediately.");
568
+ }
569
+ } finally {
570
+ lock.release();
571
+ }
572
+ return;
573
+ }
574
+
575
+ if (action === "check") {
576
+ const name = validateResourceName(args._[1]);
577
+ const resource = state.resources[name];
578
+ if (!resource) throw new Error(`local resource is not registered: ${name}`);
579
+ const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true });
580
+ const result = { name, ...inspected, contents_exposed: false };
581
+ if (args.json) console.log(JSON.stringify(result, null, 2));
582
+ else console.log(`${name}: available (${inspected.mode || "n/a"}, ${inspected.size} bytes)`);
583
+ return;
584
+ }
585
+
586
+ throw new Error(`Unknown resource action: ${action}`);
587
+ }
588
+
589
+ async function jobCommand(args) {
590
+ const action = String(args._[0] || "list").toLowerCase();
591
+ const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
592
+ const state = loadState(workspace, { stateDir: args.stateDir });
593
+ const manager = new ManagedJobManager({
594
+ jobRoot: join(state.paths.profileDir, "jobs"),
595
+ workspace,
596
+ policy: resolvePolicy({}, state.policy),
597
+ resources: state.resources,
598
+ resourceStatePath: state.paths.statePath,
599
+ logger: createLogger({ level: "warn", component: "job" }),
600
+ });
601
+ let result;
602
+ if (action === "list") result = manager.list({ limit: 50 });
603
+ else if (action === "read") result = manager.read({ job_id: args._[1] });
604
+ else if (action === "inspect") result = manager.inspectLocal({ job_id: args._[1] });
605
+ else if (action === "cancel") result = manager.cancel({ job_id: args._[1] });
606
+ else if (action === "approve") {
607
+ if (args.json && !args.yes) throw new Error("job approve --json requires --yes");
608
+ const inspection = manager.inspectLocal({ job_id: args._[1] });
609
+ if (!args.yes) {
610
+ console.log(JSON.stringify(inspection, null, 2));
611
+ const approved = await confirm(`Approve and execute managed job ${args._[1]}?`, false);
612
+ if (!approved) {
613
+ console.log("Managed job approval cancelled. Re-run with --yes after review to skip confirmation.");
614
+ return;
615
+ }
616
+ }
617
+ result = manager.approve({ job_id: args._[1] }, { localOperator: true });
618
+ }
619
+ else if (action === "submit") {
620
+ const planPath = args._[1];
621
+ if (!planPath) throw new Error("job submit requires a JSON plan file");
622
+ const plan = loadManagedJobPlan(expandHome(planPath));
623
+ result = manager.start(plan);
624
+ } else throw new Error(`Unknown job action: ${action}`);
625
+ console.log(JSON.stringify(result, null, 2));
443
626
  }
444
627
 
445
628
  async function clientConfigCommand(args) {
@@ -741,16 +924,56 @@ async function doctorCommand(args) {
741
924
  const storedPolicyOrigin = state.policy?.origin;
742
925
  state.policy = resolvePolicy({}, state.policy);
743
926
  checks.push({ name: "policy", ok: true, detail: formatPolicySummary(state.policy) });
927
+ if (state.policy.profile === "full") {
928
+ try {
929
+ assertCanonicalFullPolicy(state.policy);
930
+ checks.push({ name: "full-policy-contract", ok: true, detail: `${toolsForPolicy(state.policy).length} tools exposed` });
931
+ } catch (error) {
932
+ checks.push({ name: "full-policy-contract", ok: false, detail: sanitizeLines(error?.message || error) });
933
+ }
934
+ }
744
935
  const health = state.worker?.url ? await workerHealth(state.worker.url) : { ok: false, error: "no worker url" };
745
936
  checks.push({ name: "worker-health", ok: health.ok, detail: health.ok ? state.worker.url : health.error });
937
+ const diagnosticRuntime = new LocalDaemon({
938
+ workspace,
939
+ policy: state.policy,
940
+ logger: createLogger({ level: "error", component: "doctor" }),
941
+ jobRoot: join(state.paths.profileDir, "jobs"),
942
+ resources: state.resources,
943
+ resourceStatePath: state.paths.statePath,
944
+ recoverJobs: false,
945
+ });
946
+ let runtimeDiagnostics;
947
+ try {
948
+ runtimeDiagnostics = await diagnosticRuntime.diagnoseRuntime();
949
+ } finally {
950
+ diagnosticRuntime.stop();
951
+ }
952
+ for (const check of runtimeDiagnostics.checks) {
953
+ checks.push({
954
+ name: `runtime:${check.layer}`,
955
+ ok: check.skipped === true || check.ok === true,
956
+ detail: check.skipped ? `skipped (${check.error_class || "not applicable"})` : check.ok ? "ok" : check.error_class || "failed",
957
+ });
958
+ }
746
959
  console.log(JSON.stringify({
747
960
  ok: checks.every(check => check.ok),
748
961
  checks,
962
+ runtimeDiagnostics,
749
963
  policyMigrationPending: !storedPolicyOrigin && state.policy.origin === "migrated",
750
964
  state: redactState(state),
751
965
  }, null, 2));
752
966
  }
753
967
 
968
+ async function fullTestCommand(args) {
969
+ const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
970
+ const state = loadState(workspace, { stateDir: args.stateDir });
971
+ const policy = resolvePolicy({}, state.policy);
972
+ const result = await runFullAccessTest({ workspace, policy });
973
+ console.log(JSON.stringify(result, null, 2));
974
+ if (!result.ok) process.exitCode = 1;
975
+ }
976
+
754
977
  async function rotateSecretsCommand(args) {
755
978
  const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
756
979
  const state = loadState(workspace, { stateDir: args.stateDir });
@@ -847,6 +1070,12 @@ async function uninstallCommand(args) {
847
1070
  console.log("Uninstall cancelled. Re-run with `machine-mcp uninstall --yes` to skip confirmation.");
848
1071
  return;
849
1072
  }
1073
+ const activeJobs = activeStateJobs(stateRoot);
1074
+ if (activeJobs.length) {
1075
+ const detail = activeJobs.slice(0, 5).map((item) => `${item.job_id}:${item.status}`).join(", ");
1076
+ const suffix = activeJobs.length > 5 ? `, and ${activeJobs.length - 5} more` : "";
1077
+ throw new Error(`refusing to uninstall while managed jobs are active (${detail}${suffix}); inspect or cancel them with machine-mcp job list/cancel`);
1078
+ }
850
1079
  const autostartRemoved = await removeAutostartBestEffort(stateRoot);
851
1080
  if (!autostartRemoved) throw new Error("autostart removal failed; state and Worker were kept so the uninstall can be retried safely");
852
1081
  await sleep(500);
@@ -918,6 +1147,19 @@ async function removeAutostartBestEffort(stateRoot) {
918
1147
  }
919
1148
  }
920
1149
 
1150
+ function activeStateJobs(stateRoot) {
1151
+ const profiles = resolve(expandHome(stateRoot), "profiles");
1152
+ if (!existsSync(profiles)) return [];
1153
+ const active = [];
1154
+ for (const profile of readdirSync(profiles, { withFileTypes: true })) {
1155
+ if (!profile.isDirectory()) continue;
1156
+ for (const job of activeManagedJobs(resolve(profiles, profile.name, "jobs"))) {
1157
+ active.push({ profile: profile.name, ...job });
1158
+ }
1159
+ }
1160
+ return active;
1161
+ }
1162
+
921
1163
  function activeStateLocks(stateRoot) {
922
1164
  const profiles = resolve(expandHome(stateRoot), "profiles");
923
1165
  if (!existsSync(profiles)) return [];
@@ -1003,7 +1245,10 @@ Commands:
1003
1245
  service uninstall Remove only the autostart entry
1004
1246
  status Print redacted local profile state and Worker health
1005
1247
  doctor Check Node, Wrangler, Cloudflare login, Worker health
1248
+ full-test Run real local full-profile capability tests in a temporary sandbox
1006
1249
  rotate-secrets Rotate MCP password and daemon secret in local state
1250
+ resource generate-ssh-key NAME [PATH]
1251
+ Generate/reuse an Ed25519 key locally and register its private file by alias
1007
1252
  uninstall Delete known Worker(s), remove autostart and local state
1008
1253
 
1009
1254
  Start options:
@@ -1027,6 +1272,8 @@ Start options:
1027
1272
  --log-level LEVEL error, warn, info (default), or debug
1028
1273
  --verbose Alias for --log-level debug; includes per-tool success/correlation logs
1029
1274
  --quiet Alias for --log-level error
1275
+ --allow-insecure-permissions
1276
+ Permit resource registration when a file is group/other-readable
1030
1277
 
1031
1278
  Uninstall options:
1032
1279
  --keep-worker Do not delete deployed Worker(s) during uninstall
@@ -9,8 +9,11 @@ import { applyUpdateHunks, parsePatchEnvelope } from "./patch.mjs";
9
9
  import { executionEnv, workspaceShellCommand } from "./shell.mjs";
10
10
  import { MAX_COMMAND_BYTES, ProcessSessionManager, terminateProcessTree, validateArgv } from "./process-sessions.mjs";
11
11
  export { MAX_COMMAND_BYTES } from "./process-sessions.mjs";
12
- import { MCP_PROTOCOL_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS, normalizePolicy, SERVER_NAME, toolNamesForPolicy } from "./tools.mjs";
12
+ import { allToolNames, assertCanonicalFullPolicy, isCanonicalFullPolicy, MCP_PROTOCOL_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS, normalizePolicy, POLICY_PROFILES, SERVER_NAME, toolNamesForPolicy } from "./tools.mjs";
13
13
  import { classifyOperationalError } from "./log.mjs";
14
+ import { ManagedJobManager } from "./managed-jobs.mjs";
15
+ import { generateRegisteredSshKey } from "./resource-operations.mjs";
16
+ import { expandHome } from "./state.mjs";
14
17
 
15
18
  export const MAX_WRITE_BYTES = 5 * 1024 * 1024;
16
19
  const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
@@ -22,7 +25,7 @@ const MAX_IMAGE_BYTES = 4 * 1024 * 1024;
22
25
  const SLOW_TOOL_CALL_MS = 30_000;
23
26
 
24
27
  export class LocalDaemon {
25
- constructor({ workerUrl = "", secret = "", workspace, policy, logger = console, onSuperseded = null }) {
28
+ constructor({ workerUrl = "", secret = "", workspace, policy, logger = console, onSuperseded = null, jobRoot = "", resources = {}, resourceStatePath = "", recoverJobs = true }) {
26
29
  this.workerUrl = workerUrl ? normalizeWorkerUrl(workerUrl) : "";
27
30
  if (this.workerUrl && (typeof secret !== "string" || secret.length < 16)) throw new Error("daemon secret is missing or too short");
28
31
  this.secret = secret || "";
@@ -32,6 +35,7 @@ export class LocalDaemon {
32
35
  this.policy = normalizePolicy(policy);
33
36
  this.logger = logger;
34
37
  this.onSuperseded = typeof onSuperseded === "function" ? onSuperseded : null;
38
+ this.resourceStatePath = resourceStatePath ? resolve(resourceStatePath) : "";
35
39
  this.closed = false;
36
40
  this.ws = null;
37
41
  this.heartbeat = null;
@@ -46,6 +50,16 @@ export class LocalDaemon {
46
50
  this.reconnectAttempt = 0;
47
51
  this.mutationQueue = Promise.resolve();
48
52
  this.runtimeDir = createRuntimeDir();
53
+ if (typeof jobRoot !== "string" || !jobRoot.trim()) throw new Error("persistent managed-job root is required");
54
+ this.managedJobManager = new ManagedJobManager({
55
+ jobRoot,
56
+ workspace: this.workspace,
57
+ policy: this.policy,
58
+ resources,
59
+ resourceStatePath,
60
+ logger: this.logger,
61
+ recover: recoverJobs,
62
+ });
49
63
  this.processSessionManager = new ProcessSessionManager({
50
64
  workspace: this.workspace,
51
65
  policy: this.policy,
@@ -74,6 +88,11 @@ export class LocalDaemon {
74
88
  workspace: this.displayPath(this.workspace),
75
89
  workspace_name: basename(this.workspace),
76
90
  policy: this.policy,
91
+ policy_contract: {
92
+ named_profile_is_canonical: this.policy.profile === "custom" || policyMatchesNamedProfile(this.policy),
93
+ full_catalog_complete: this.policy.profile === "full" ? isCanonicalFullPolicy(this.policy) && this.tools().length + 1 === allToolNames().length : null,
94
+ machine_bridge_internal_denials_under_full: this.policy.profile === "full" && isCanonicalFullPolicy(this.policy) ? false : null,
95
+ },
77
96
  enforcement: {
78
97
  filesystem_scope: this.policy.unrestrictedPaths ? "local-user-accessible" : "workspace",
79
98
  sensitive_filename_filter: false,
@@ -81,10 +100,17 @@ export class LocalDaemon {
81
100
  host_policy_is_independent: true,
82
101
  },
83
102
  tools: ["server_info", ...this.tools()],
103
+ observability: {
104
+ per_tool_events: "debug-only",
105
+ default_logs_include_tool_failures: false,
106
+ tool_arguments_or_results_logged: false,
107
+ },
84
108
  runtime: {
85
109
  environment: this.policy.minimalEnv ? "isolated-minimal" : "full-parent",
86
110
  runtime_dir: this.policy.exposeAbsolutePaths ? this.runtimeDir : "<private-runtime-dir>",
87
111
  process_sessions: this.processSessionManager.status(),
112
+ managed_jobs: this.managedJobManager.status(),
113
+ local_resources: this.managedJobManager.resourceInfo(),
88
114
  },
89
115
  };
90
116
  }
@@ -238,14 +264,12 @@ export class LocalDaemon {
238
264
  if (this.cancelledCalls.has(id)) throw new Error("tool call cancelled");
239
265
  this.send({ type: "tool_result", id, ok: true, result });
240
266
  const durationMs = Date.now() - started;
241
- if (durationMs >= SLOW_TOOL_CALL_MS) this.logger.info?.("slow tool call completed", { tool, duration_ms: durationMs });
242
- else this.logger.debug?.("tool call completed", { call_id: shortCallId(id), tool, duration_ms: durationMs });
267
+ this.logger.debug?.(durationMs >= SLOW_TOOL_CALL_MS ? "slow tool call completed" : "tool call completed", { call_id: shortCallId(id), tool, duration_ms: durationMs });
243
268
  } catch (error) {
244
269
  const safeError = this.safeErrorMessage(error);
245
270
  this.send({ type: "tool_result", id, ok: false, error: { message: safeError } });
246
271
  const durationMs = Date.now() - started;
247
- this.logger.warn?.("tool call failed", { tool, duration_ms: durationMs, error_class: classifyOperationalError(error) });
248
- this.logger.debug?.("tool call failure correlation", { call_id: shortCallId(id) });
272
+ this.logger.debug?.("tool call failed", { call_id: shortCallId(id), tool, duration_ms: durationMs, error_class: classifyOperationalError(error) });
249
273
  } finally {
250
274
  clearTimeout(deadline);
251
275
  this.activeToolCalls -= 1;
@@ -291,6 +315,14 @@ export class LocalDaemon {
291
315
  case "git_diff": return this.gitDiff(args, context);
292
316
  case "git_log": return this.gitLog(args, context);
293
317
  case "git_show": return this.gitShow(args, context);
318
+ case "diagnose_runtime": return this.diagnoseRuntime(context);
319
+ case "list_local_resources": return this.managedJobManager.listResources();
320
+ case "generate_ssh_key_resource": return this.generateSshKeyResource(args, context);
321
+ case "stage_job": return this.managedJobManager.stage(args);
322
+ case "start_job": return this.managedJobManager.start(args);
323
+ case "list_jobs": return this.managedJobManager.list(args);
324
+ case "read_job": return this.managedJobManager.read(args);
325
+ case "cancel_job": return this.managedJobManager.cancel(args);
294
326
  case "run_process": return this.runDirectProcess(args, context);
295
327
  case "start_process": return this.processSessionManager.start(args, context);
296
328
  case "read_process": return this.processSessionManager.read(args, context);
@@ -629,6 +661,117 @@ export class LocalDaemon {
629
661
  return { ok: true, target, root, pathspec: repoRelative || "" };
630
662
  }
631
663
 
664
+ async diagnoseRuntime(context = {}) {
665
+ this.throwIfCancelled(context);
666
+ const checks = [];
667
+ checks.push({
668
+ layer: "mcp-host-to-daemon",
669
+ ok: true,
670
+ detail: "This diagnostic request reached the local Machine Bridge runtime.",
671
+ });
672
+ checks.push({
673
+ layer: "machine-bridge-policy",
674
+ ok: this.policy.execMode === "direct" || this.policy.execMode === "shell",
675
+ detail: `profile=${this.policy.profile}; exec_mode=${this.policy.execMode}; unrestricted_paths=${this.policy.unrestrictedPaths}`,
676
+ });
677
+
678
+ const probe = join(this.runtimeDir, `.diagnostic-${process.pid}-${randomBytes(6).toString("hex")}`);
679
+ try {
680
+ await writeFile(probe, "ok\n", { mode: 0o600, flag: "wx" });
681
+ const { buffer } = await readBoundedFile(probe, 64, "diagnostic file");
682
+ checks.push({ layer: "local-filesystem", ok: buffer.toString("utf8") === "ok\n", error_class: null });
683
+ } catch (error) {
684
+ checks.push({ layer: "local-filesystem", ok: false, error_class: classifyOperationalError(error) });
685
+ } finally {
686
+ await rm(probe, { force: true }).catch(() => {});
687
+ }
688
+
689
+ if (this.policy.execMode === "direct" || this.policy.execMode === "shell") {
690
+ const direct = await this.runProcess(
691
+ process.execPath,
692
+ ["-e", "process.stdout.write('ok')"],
693
+ 5000,
694
+ true,
695
+ 1024,
696
+ context,
697
+ this.workspace,
698
+ ).catch((error) => ({ code: 127, stdout: "", stderr: "", error_class: classifyOperationalError(error) }));
699
+ checks.push({
700
+ layer: "local-process-spawn",
701
+ ok: direct.code === 0 && direct.stdout === "ok",
702
+ error_class: direct.error_class || (direct.code === 0 ? null : classifyOperationalError(direct.stderr || direct.stdout || "execution failed")),
703
+ });
704
+ } else {
705
+ checks.push({ layer: "local-process-spawn", ok: false, skipped: true, error_class: "policy_denied" });
706
+ }
707
+
708
+ if (this.policy.execMode === "shell") {
709
+ const shell = workspaceShellCommand(process.platform === "win32" ? "cd" : "pwd");
710
+ const result = await this.runProcess(shell.cmd, shell.args, 5000, true, 4096, context, this.workspace)
711
+ .catch((error) => ({ code: 127, error_class: classifyOperationalError(error) }));
712
+ checks.push({
713
+ layer: "local-shell",
714
+ ok: result.code === 0,
715
+ error_class: result.error_class || (result.code === 0 ? null : classifyOperationalError(result.stderr || result.stdout || "execution failed")),
716
+ });
717
+ } else {
718
+ checks.push({ layer: "local-shell", ok: false, skipped: true, error_class: "policy_denied" });
719
+ }
720
+
721
+ const storage = this.managedJobManager.diagnoseStorage();
722
+ checks.push({ layer: "managed-job-storage", ...storage });
723
+ const resources = this.managedJobManager.listResources();
724
+ checks.push({
725
+ layer: "local-resource-registry",
726
+ ok: resources.resources.every((resource) => resource.available),
727
+ registered: resources.count,
728
+ unavailable: resources.resources.filter((resource) => !resource.available).map((resource) => ({ name: resource.name, error_class: resource.error_class })),
729
+ });
730
+
731
+ return {
732
+ request_reached_local_runtime: true,
733
+ interpretation: {
734
+ tool_call_blocked_before_response: "host/platform or connector gateway",
735
+ diagnostic_reached_daemon_but_spawn_failed: "local OS, endpoint security, shell configuration, or Machine Bridge policy",
736
+ managed_job_accepted_then_later_tools_blocked: "job continues independently; inspect with local CLI or a later read_job call",
737
+ },
738
+ policy: this.policy,
739
+ checks,
740
+ ok: checks.filter((check) => !check.skipped).every((check) => check.ok),
741
+ };
742
+ }
743
+
744
+ async generateSshKeyResource(args = {}, context = {}) {
745
+ this.throwIfCancelled(context);
746
+ assertCanonicalFullPolicy(this.policy);
747
+ if (!this.resourceStatePath) throw new Error("local resource state is unavailable in this runtime");
748
+ const home = process.env.HOME || process.env.USERPROFILE;
749
+ if (!home) throw new Error("HOME or USERPROFILE is required to choose a default SSH key path");
750
+ const target = args.path
751
+ ? resolve(expandHome(String(args.path)))
752
+ : resolve(home, ".ssh", `machine-mcp-${args.name}-ed25519`);
753
+ const key = await generateRegisteredSshKey({
754
+ workspace: this.workspace,
755
+ stateDir: stateRootFromProfileStatePath(this.resourceStatePath),
756
+ name: args.name,
757
+ targetPath: target,
758
+ comment: args.comment || `machine-mcp:${args.name}`,
759
+ });
760
+ return {
761
+ name: key.name,
762
+ created: key.created,
763
+ registered: key.registered,
764
+ private_key_path: this.displayPath(key.privateKeyPath),
765
+ public_key_path: this.displayPath(key.publicKeyPath),
766
+ fingerprint: key.fingerprint,
767
+ key_type: key.keyType,
768
+ private_mode: key.privateMode,
769
+ public_mode: key.publicMode,
770
+ private_key_content_exposed: key.privateKeyContentExposed,
771
+ available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
772
+ };
773
+ }
774
+
632
775
  async runDirectProcess(args, context = {}) {
633
776
  if (this.policy.execMode !== "direct" && this.policy.execMode !== "shell") throw new Error("run_process is disabled by daemon policy");
634
777
  const argv = validateArgv(args.argv);
@@ -831,6 +974,25 @@ export class LocalDaemon {
831
974
  }
832
975
  }
833
976
 
977
+ function policyMatchesNamedProfile(policy) {
978
+ const named = POLICY_PROFILES[policy.profile];
979
+ if (!named) return false;
980
+ return policy.allowWrite === named.allowWrite
981
+ && policy.execMode === named.execMode
982
+ && policy.unrestrictedPaths === named.unrestrictedPaths
983
+ && policy.minimalEnv === named.minimalEnv
984
+ && policy.exposeAbsolutePaths === named.exposeAbsolutePaths;
985
+ }
986
+
987
+ function stateRootFromProfileStatePath(statePath) {
988
+ const absolute = resolve(statePath);
989
+ if (basename(absolute) !== "state.json") throw new Error("local resource state path is invalid");
990
+ const profileDir = dirname(absolute);
991
+ const profilesDir = dirname(profileDir);
992
+ if (basename(profilesDir) !== "profiles") throw new Error("local resource state path is outside the expected profile layout");
993
+ return dirname(profilesDir);
994
+ }
995
+
834
996
  function normalizeWorkerUrl(value) {
835
997
  let url;
836
998
  try { url = new URL(String(value || "")); } catch { throw new Error("invalid Worker URL"); }