machine-bridge-mcp 0.4.2 → 0.5.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/src/local/cli.mjs CHANGED
@@ -1,12 +1,12 @@
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
3
  import path, { 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
10
  import { runWrangler } from "./shell.mjs";
11
11
  import {
12
12
  acquireDaemonLock,
@@ -37,10 +37,10 @@ const BOOLEAN_OPTIONS = new Set([
37
37
  "help", "version", "quiet", "json", "verbose", "rotateSecrets", "forceWorker",
38
38
  "daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials",
39
39
  "printCredentials", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
40
- "yes", "keepWorker", "noApi", "api", "rotateApiKey",
40
+ "yes", "keepWorker",
41
41
  ]);
42
42
  const VALUE_OPTIONS = new Set([
43
- "workspace", "stateDir", "workerName", "profile", "execMode", "client", "apiPort", "apiHost", "apiKey", "apiModel", "port",
43
+ "workspace", "stateDir", "workerName", "profile", "execMode", "client", "logLevel",
44
44
  ]);
45
45
 
46
46
  export async function main(argv = process.argv.slice(2)) {
@@ -51,6 +51,7 @@ export async function main(argv = process.argv.slice(2)) {
51
51
  if (command === "api") throw removedLocalApiError();
52
52
  validateCommandOptions(command, args);
53
53
  validatePositionals(command, args);
54
+ validateLoggingOptions(args);
54
55
 
55
56
  switch (command) {
56
57
  case "start": return startCommand(args);
@@ -72,16 +73,15 @@ export async function main(argv = process.argv.slice(2)) {
72
73
 
73
74
  const COMMAND_OPTIONS = {
74
75
  start: new Set([
75
- "workspace", "stateDir", "workerName", "quiet", "json", "verbose", "rotateSecrets", "forceWorker",
76
+ "workspace", "stateDir", "workerName", "quiet", "json", "verbose", "logLevel", "rotateSecrets", "forceWorker",
76
77
  "daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials", "printCredentials",
77
78
  "profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
78
- "noApi", "api", "apiPort", "apiHost", "apiKey", "rotateApiKey", "apiModel", "port",
79
79
  ]),
80
- stdio: new Set(["workspace", "stateDir", "profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths", "verbose"]),
80
+ stdio: new Set(["workspace", "stateDir", "profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths", "verbose", "quiet", "logLevel"]),
81
81
  "client-config": new Set(["workspace", "stateDir", "profile", "client", "json"]),
82
82
  status: new Set(["workspace", "stateDir"]),
83
83
  doctor: new Set(["workspace", "stateDir"]),
84
- "rotate-secrets": new Set(["workspace", "stateDir", "workerName", "noPrintCredentials", "quiet"]),
84
+ "rotate-secrets": new Set(["workspace", "stateDir", "workerName", "noPrintCredentials", "printMcpCredentials", "printCredentials", "quiet"]),
85
85
  workspace: new Set(["workspace", "stateDir"]),
86
86
  service: new Set(["workspace", "stateDir", "quiet"]),
87
87
  autostart: new Set(["workspace", "stateDir", "quiet"]),
@@ -97,6 +97,21 @@ export function validateCommandOptions(command, args) {
97
97
  }
98
98
  }
99
99
 
100
+ export function validateLoggingOptions(args = {}) {
101
+ if (args.quiet && args.verbose) throw new Error("--quiet and --verbose cannot be used together");
102
+ if (args.logLevel !== undefined && (args.quiet || args.verbose)) {
103
+ throw new Error("--log-level cannot be combined with --quiet or --verbose");
104
+ }
105
+ if (args.logLevel !== undefined) normalizeLogLevel(args.logLevel);
106
+ }
107
+
108
+ function effectiveLogLevel(args = {}) {
109
+ if (args.logLevel !== undefined) return normalizeLogLevel(args.logLevel);
110
+ if (args.quiet) return "error";
111
+ if (args.verbose) return "debug";
112
+ return "info";
113
+ }
114
+
100
115
  function toKebab(value) {
101
116
  return String(value).replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
102
117
  }
@@ -256,8 +271,7 @@ async function confirm(prompt, assumeYes = false) {
256
271
 
257
272
  async function startCommand(args) {
258
273
  assertNodeVersion();
259
- assertNoRemovedLocalApiOptions(args);
260
- const logger = createLogger({ quiet: Boolean(args.quiet || args.json), verbose: Boolean(args.verbose), component: "cli" });
274
+ const logger = createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "cli" });
261
275
  const workspace = await chooseWorkspace(args, { promptOnFirstRun: true, save: true, allowPositional: true });
262
276
  const state = loadState(workspace, { stateDir: args.stateDir });
263
277
  const startupLock = acquireStartupLock(state);
@@ -273,7 +287,7 @@ async function startCommand(args) {
273
287
  } else {
274
288
  // Stop an installed service before acquiring the runtime lock. If a
275
289
  // foreground daemon owns the lock, no new policy or secret state is saved.
276
- await stopAutostartBestEffort(Boolean(args.quiet));
290
+ await stopAutostartBestEffort(logger);
277
291
  }
278
292
 
279
293
  const lock = acquireDaemonLock(state);
@@ -281,7 +295,7 @@ async function startCommand(args) {
281
295
  const pid = lock.owner?.pid ? `pid ${lock.owner.pid}` : "unknown pid";
282
296
  logger.warn(`local daemon already running for this workspace (${pid}); requested changes were not applied`);
283
297
  if (args.json) printStartJson(state, {
284
- noPrintCredentials: Boolean(args.noPrintCredentials),
298
+ showCredentials: Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials),
285
299
  requestedChangesApplied: false,
286
300
  notice: "local daemon already running; requested changes were not applied",
287
301
  });
@@ -289,6 +303,7 @@ async function startCommand(args) {
289
303
  noPrintCredentials: Boolean(args.noPrintCredentials),
290
304
  includeCredentials: Boolean(args.printMcpCredentials || args.printCredentials),
291
305
  quiet: Boolean(args.quiet),
306
+ verbose: Boolean(args.verbose),
292
307
  });
293
308
  return;
294
309
  }
@@ -299,7 +314,9 @@ async function startCommand(args) {
299
314
  const firstMcpConnection = !previousMcpServerUrl || !state.worker?.oauthPassword;
300
315
  const workerName = validateWorkerName(args.workerName);
301
316
  ensureWorkerSecrets(state, { rotateSecrets: Boolean(args.rotateSecrets), workerName });
317
+ const previousPolicyOrigin = state.policy?.origin;
302
318
  state.policy = resolvePolicy(args, state.policy);
319
+ const policyMigrated = !previousPolicyOrigin && state.policy.origin === "migrated";
303
320
  state.policy.updatedAt = new Date().toISOString();
304
321
  saveState(state);
305
322
 
@@ -307,10 +324,10 @@ async function startCommand(args) {
307
324
  else if (!state.worker.url) throw new Error("--daemon-only requires an existing worker URL in state; run start once without --daemon-only");
308
325
 
309
326
  const mcpConnectionChanged = previousMcpServerUrl && previousMcpServerUrl !== state.worker.mcpServerUrl;
310
- const shouldPrintMcpCredentials = Boolean(args.json || args.printMcpCredentials || args.printCredentials || firstMcpConnection || args.rotateSecrets || mcpConnectionChanged);
327
+ const shouldPrintMcpCredentials = Boolean(args.printMcpCredentials || args.printCredentials || firstMcpConnection || args.rotateSecrets || mcpConnectionChanged);
311
328
 
312
329
  if (!args.daemonOnly && !args.noAutostart) {
313
- await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1] });
330
+ await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1], logger });
314
331
  }
315
332
 
316
333
  daemon = new LocalDaemon({
@@ -318,7 +335,7 @@ async function startCommand(args) {
318
335
  secret: state.worker.daemonSecret,
319
336
  workspace,
320
337
  policy: state.policy,
321
- logger: createLogger({ quiet: Boolean(args.quiet || args.json), verbose: Boolean(args.verbose), component: "daemon" }),
338
+ logger: createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "daemon" }),
322
339
  onSuperseded: () => {
323
340
  logger.warn("this daemon was replaced by a newer authenticated instance; exiting without reconnecting");
324
341
  lock.release();
@@ -327,13 +344,18 @@ async function startCommand(args) {
327
344
  });
328
345
 
329
346
  const waitForConnect = daemon.start();
330
- await waitForConnectWithNotice(waitForConnect, 20_000, Boolean(args.quiet || args.json));
331
- if (args.json) printStartJson(state, { noPrintCredentials: Boolean(args.noPrintCredentials) });
347
+ await waitForConnectWithNotice(waitForConnect, 20_000, logger);
348
+ if (args.json) printStartJson(state, {
349
+ showCredentials: Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials),
350
+ notice: policyMigrated ? "legacy implicit policy migrated to full access" : "",
351
+ });
332
352
  else {
333
353
  printMcpConnection(state, {
334
354
  noPrintCredentials: Boolean(args.noPrintCredentials),
335
355
  includeCredentials: shouldPrintMcpCredentials,
336
356
  quiet: Boolean(args.quiet),
357
+ verbose: Boolean(args.verbose),
358
+ policyMigrated,
337
359
  });
338
360
  }
339
361
  keepProcessAlive({ daemon, lock, logger });
@@ -347,13 +369,6 @@ async function startCommand(args) {
347
369
  }
348
370
  }
349
371
 
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
372
  export function resolvePolicy(args = {}, stored = {}) {
358
373
  const hasStored = stored && typeof stored === "object" && (
359
374
  typeof stored.allowWrite === "boolean" || typeof stored.allowExec === "boolean" || typeof stored.execMode === "string"
@@ -361,14 +376,15 @@ export function resolvePolicy(args = {}, stored = {}) {
361
376
  const explicitKeys = ["profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths"];
362
377
  const hasExplicit = explicitKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key));
363
378
  let base;
379
+
364
380
  if (args.profile !== undefined) {
365
381
  const profile = String(args.profile).trim().toLowerCase();
366
382
  if (!POLICY_PROFILES[profile]) throw new Error(`--profile must be one of: ${Object.keys(POLICY_PROFILES).join(", ")}`);
367
- base = { ...POLICY_PROFILES[profile] };
383
+ base = policyProfile(profile, "explicit");
368
384
  } else if (hasStored) {
369
- base = normalizePolicy(stored);
385
+ base = migrateLegacyPolicy(stored);
370
386
  } else {
371
- base = { ...POLICY_PROFILES.full };
387
+ base = policyProfile(DEFAULT_POLICY_PROFILE, "default");
372
388
  }
373
389
 
374
390
  if (!hasExplicit) return normalizePolicy(base);
@@ -388,16 +404,42 @@ export function resolvePolicy(args = {}, stored = {}) {
388
404
  if (args.absolutePaths === true) base.exposeAbsolutePaths = true;
389
405
  if (args.absolutePaths === false) base.exposeAbsolutePaths = false;
390
406
  const overrideKeys = ["execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths"];
391
- if (args.profile === undefined || overrideKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key))) base.profile = "custom";
407
+ if (args.profile === undefined || overrideKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key))) {
408
+ base.profile = "custom";
409
+ base.origin = "custom";
410
+ base.revision = DEFAULT_POLICY_REVISION;
411
+ }
392
412
  return normalizePolicy(base);
393
413
  }
394
414
 
415
+ function migrateLegacyPolicy(stored = {}) {
416
+ if (stored.origin === "default" && Number(stored.revision || 0) < DEFAULT_POLICY_REVISION) {
417
+ return policyProfile(DEFAULT_POLICY_PROFILE, "default");
418
+ }
419
+ if (stored.origin === "migrated" && Number(stored.revision || 0) < DEFAULT_POLICY_REVISION) {
420
+ return policyProfile(DEFAULT_POLICY_PROFILE, "migrated");
421
+ }
422
+ if (stored.origin) return normalizePolicy(stored);
423
+ const normalized = normalizePolicy(stored);
424
+ const looksLikeLegacyImplicitDefault = (
425
+ normalized.profile === "custom" &&
426
+ normalized.allowWrite === true &&
427
+ normalized.execMode === "shell" &&
428
+ normalized.unrestrictedPaths === false &&
429
+ normalized.minimalEnv === true &&
430
+ normalized.exposeAbsolutePaths === false
431
+ );
432
+ if (looksLikeLegacyImplicitDefault) return policyProfile("full", "migrated");
433
+ return normalizePolicy({ ...normalized, origin: "legacy-preserved", revision: DEFAULT_POLICY_REVISION });
434
+ }
435
+
436
+
395
437
  async function stdioCommand(args) {
396
438
  assertNodeVersion();
397
439
  const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
398
440
  const state = loadState(workspace, { stateDir: args.stateDir });
399
441
  const policy = resolvePolicy(args, state.policy);
400
- await runStdioServer({ workspace, policy, verbose: Boolean(args.verbose) });
442
+ await runStdioServer({ workspace, policy, logLevel: effectiveLogLevel(args) });
401
443
  }
402
444
 
403
445
  async function clientConfigCommand(args) {
@@ -426,17 +468,13 @@ async function clientConfigCommand(args) {
426
468
  }
427
469
  }
428
470
 
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
471
  function removedLocalApiError() {
435
472
  return new Error("Local /v1 API support has been removed. Use the printed Remote MCP Server URL/password with an MCP client instead.");
436
473
  }
437
474
 
475
+
438
476
  async function ensureWorker(state, args) {
439
- const logger = createLogger({ quiet: Boolean(args.quiet || args.json), verbose: Boolean(args.verbose), component: "worker" });
477
+ const logger = createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "worker" });
440
478
  const desiredHash = workerDeployHash(state);
441
479
  const expectedVersion = currentPackageVersion();
442
480
  const complete = state.worker.url && state.worker.mcpServerUrl && state.worker.oauthPassword && state.worker.daemonSecret && state.worker.oauthTokenVersion && state.worker.name;
@@ -487,7 +525,7 @@ async function withSecretsFile(state, callback) {
487
525
  const dir = state.paths.profileDir;
488
526
  ensureOwnerOnlyDir(dir);
489
527
  cleanupStaleSecretFiles(dir);
490
- const tempPath = resolve(dir, `worker-secrets-${process.pid}-${Date.now()}.json`);
528
+ const tempPath = resolve(dir, `worker-secrets-${process.pid}-${Date.now()}-${randomBytes(6).toString("hex")}.json`);
491
529
  const payload = {
492
530
  MCP_OAUTH_PASSWORD: state.worker.oauthPassword,
493
531
  DAEMON_SHARED_SECRET: state.worker.daemonSecret,
@@ -504,10 +542,14 @@ async function withSecretsFile(state, callback) {
504
542
 
505
543
  function cleanupStaleSecretFiles(dir) {
506
544
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
507
- if (!entry.isFile() || !/^worker-secrets-.*\.json$/.test(entry.name)) continue;
545
+ if (!entry.isFile()) continue;
546
+ const match = /^worker-secrets-(\d+)-(\d+)(?:-[a-f0-9]+)?\.json$/.exec(entry.name);
547
+ if (!match) continue;
508
548
  const file = resolve(dir, entry.name);
509
549
  try {
510
- if (Date.now() - statSync(file).mtimeMs > 60 * 60 * 1000) unlinkSync(file);
550
+ const pid = Number(match[1]);
551
+ const ageMs = Date.now() - statSync(file).mtimeMs;
552
+ if (!isPidAlive(pid) || ageMs > 60 * 60 * 1000) unlinkSync(file);
511
553
  } catch {}
512
554
  }
513
555
  }
@@ -562,10 +604,18 @@ async function workerHealth(workerUrl, expectedVersion = currentPackageVersion()
562
604
  if (body?.version !== expectedVersion) return { ok: false, error: `version_mismatch:${body?.version || "unknown"}!=${expectedVersion}` };
563
605
  return { ok: true, version: body.version };
564
606
  } catch (error) {
565
- return { ok: false, error: error.message };
607
+ return { ok: false, error: workerHealthError(error) };
566
608
  }
567
609
  }
568
610
 
611
+ function workerHealthError(error) {
612
+ const message = String(error?.message || error || "");
613
+ if (/timeout|aborted/i.test(message)) return "timeout";
614
+ if (/certificate|TLS|SSL/i.test(message)) return "tls_error";
615
+ if (/fetch failed|network|ECONN|ENOTFOUND|EAI_AGAIN/i.test(message)) return "network_error";
616
+ return "request_failed";
617
+ }
618
+
569
619
  async function retryHealth(workerUrl, expectedVersion, attempts) {
570
620
  let last = { ok: false, error: "not_checked" };
571
621
  for (let i = 0; i < attempts; i += 1) {
@@ -583,23 +633,22 @@ function extractWorkerUrl(text = "") {
583
633
  return anyHttps.find(match => /workers\.dev|\/healthz|\/mcp/.test(match[0]))?.[0]?.replace(/[),.]+$/, "") || "";
584
634
  }
585
635
 
586
- async function waitForConnectWithNotice(promise, timeoutMs, quiet = false) {
636
+ async function waitForConnectWithNotice(promise, timeoutMs, logger) {
587
637
  let timeout;
588
638
  const timed = new Promise(resolvePromise => {
589
639
  timeout = setTimeout(() => resolvePromise("timeout"), timeoutMs);
590
640
  });
591
641
  const result = await Promise.race([promise.then(() => "connected"), timed]);
592
642
  clearTimeout(timeout);
593
- if (result === "timeout") createLogger({ component: "daemon", quiet }).warn("Still connecting; the process will keep retrying");
643
+ if (result === "timeout") logger.warn("Still connecting; the process will keep retrying");
594
644
  }
595
645
 
596
646
 
597
- function printStartJson(state, { noPrintCredentials = false, requestedChangesApplied = true, notice = "" } = {}) {
598
- const mcpPassword = noPrintCredentials ? previewSecret(state.worker.oauthPassword) : state.worker.oauthPassword;
647
+ function printStartJson(state, { showCredentials = false, requestedChangesApplied = true, notice = "" } = {}) {
599
648
  createLogger({ component: "ready" }).json({
600
649
  mcp: {
601
650
  server_url: state.worker.mcpServerUrl,
602
- connection_password: mcpPassword,
651
+ connection_password: showCredentials ? state.worker.oauthPassword : previewSecret(state.worker.oauthPassword),
603
652
  worker_url: state.worker.url,
604
653
  worker_name: state.worker.name,
605
654
  },
@@ -611,36 +660,45 @@ function printStartJson(state, { noPrintCredentials = false, requestedChangesApp
611
660
  });
612
661
  }
613
662
 
614
- function printMcpConnection(state, { json = false, noPrintCredentials = false, includeCredentials = false, quiet = false } = {}) {
615
- const logger = createLogger({ component: "ready", quiet });
663
+ function printMcpConnection(state, {
664
+ noPrintCredentials = false,
665
+ includeCredentials = false,
666
+ quiet = false,
667
+ verbose = false,
668
+ policyMigrated = false,
669
+ } = {}) {
670
+ const logger = createLogger({ component: "ready", quiet, level: quiet ? "error" : verbose ? "debug" : "info" });
616
671
  const payload = {
617
672
  mcp_server_url: state.worker.mcpServerUrl,
618
673
  mcp_connection_password: state.worker.oauthPassword,
619
- worker_url: state.worker.url,
620
- worker_name: state.worker.name,
621
674
  workspace: state.workspace.path,
622
675
  state_path: state.paths.statePath,
623
676
  policy: state.policy,
624
677
  };
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
678
  if (includeCredentials) {
631
679
  logger.success("Remote MCP bridge is ready; save these connection details if your ChatGPT app needs to reconnect");
632
680
  logger.plain(` MCP Server URL: ${payload.mcp_server_url}`);
633
681
  if (!noPrintCredentials) logger.plain(` MCP connection password: ${payload.mcp_connection_password}`);
634
682
  else logger.plain(` MCP connection password: ${previewSecret(payload.mcp_connection_password)} (redacted)`);
635
683
  } else {
636
- logger.success("Remote MCP bridge is ready; MCP connection details unchanged");
637
- logger.plain(" Use --print-mcp-credentials only when a ChatGPT app needs to reconnect.");
684
+ logger.success("Remote MCP bridge is ready");
685
+ logger.plain(" Connection credentials unchanged; use --print-mcp-credentials only when reconnecting a client.");
638
686
  }
639
- logger.plain(` Workspace cwd: ${payload.workspace}`);
640
- logger.plain(` Policy: profile=${payload.policy.profile || "custom"}, write=${payload.policy.allowWrite ? "on" : "off"}, exec_mode=${payload.policy.execMode || (payload.policy.allowExec ? "shell" : "off")}, unrestricted_paths=${payload.policy.unrestrictedPaths ? "on" : "off"}, absolute_paths=${payload.policy.exposeAbsolutePaths ? "on" : "off"}`);
641
- logger.plain(` State: ${payload.state_path}`);
687
+ if (policyMigrated) {
688
+ logger.warn("Legacy implicit policy migrated to full access; use --profile agent, edit, or review to narrow it.");
689
+ }
690
+ logger.plain(` Workspace: ${payload.workspace}`);
691
+ logger.plain(` Policy: ${formatPolicySummary(payload.policy)}`);
692
+ if (verbose) logger.plain(` State: ${payload.state_path}`);
693
+ }
694
+
695
+ function formatPolicySummary(policy = {}) {
696
+ const scope = policy.unrestrictedPaths ? "all local paths" : "workspace only";
697
+ const environment = policy.minimalEnv ? "isolated env" : "full parent env";
698
+ 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
699
  }
643
700
 
701
+
644
702
  function keepProcessAlive({ daemon = null, lock = null, logger = createLogger({ component: "cli" }) } = {}) {
645
703
  let stopping = false;
646
704
  const stop = async () => {
@@ -660,8 +718,14 @@ function keepProcessAlive({ daemon = null, lock = null, logger = createLogger({
660
718
  async function statusCommand(args) {
661
719
  const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
662
720
  const state = loadState(workspace, { stateDir: args.stateDir });
721
+ const storedPolicyOrigin = state.policy?.origin;
722
+ state.policy = resolvePolicy({}, state.policy);
663
723
  const health = state.worker?.url ? await workerHealth(state.worker.url) : { ok: false, error: "no worker url" };
664
- const payload = { ...redactState(state), workerHealth: health };
724
+ const payload = {
725
+ ...redactState(state),
726
+ policyMigrationPending: !storedPolicyOrigin && state.policy.origin === "migrated",
727
+ workerHealth: health,
728
+ };
665
729
  console.log(JSON.stringify(payload, null, 2));
666
730
  }
667
731
 
@@ -674,9 +738,17 @@ async function doctorCommand(args) {
674
738
  const whoami = await runWrangler(["whoami"], { capture: true, allowFailure: true });
675
739
  checks.push({ name: "cloudflare-login", ok: whoami.code === 0, detail: whoami.code === 0 ? "authenticated" : sanitizeLines(whoami.stderr || whoami.stdout) });
676
740
  const state = loadState(workspace, { stateDir: args.stateDir });
741
+ const storedPolicyOrigin = state.policy?.origin;
742
+ state.policy = resolvePolicy({}, state.policy);
743
+ checks.push({ name: "policy", ok: true, detail: formatPolicySummary(state.policy) });
677
744
  const health = state.worker?.url ? await workerHealth(state.worker.url) : { ok: false, error: "no worker url" };
678
745
  checks.push({ name: "worker-health", ok: health.ok, detail: health.ok ? state.worker.url : health.error });
679
- console.log(JSON.stringify({ ok: checks.every(check => check.ok), checks, state: redactState(state) }, null, 2));
746
+ console.log(JSON.stringify({
747
+ ok: checks.every(check => check.ok),
748
+ checks,
749
+ policyMigrationPending: !storedPolicyOrigin && state.policy.origin === "migrated",
750
+ state: redactState(state),
751
+ }, null, 2));
680
752
  }
681
753
 
682
754
  async function rotateSecretsCommand(args) {
@@ -688,7 +760,7 @@ async function rotateSecretsCommand(args) {
688
760
  throw new Error(`another startup/deployment operation is already running for this workspace (${pid})`);
689
761
  }
690
762
  try {
691
- await stopAutostartBestEffort(Boolean(args.quiet));
763
+ await stopAutostartBestEffort(createLogger({ level: args.quiet ? "error" : "warn", component: "service" }));
692
764
  await sleep(500);
693
765
  const daemonOwner = readDaemonLockOwner(daemonLockPathForState(state));
694
766
  if (daemonOwner?.pid && isPidAlive(daemonOwner.pid)) {
@@ -696,9 +768,10 @@ async function rotateSecretsCommand(args) {
696
768
  }
697
769
  ensureWorkerSecrets(state, { rotateSecrets: true, workerName: validateWorkerName(args.workerName) });
698
770
  saveState(state);
699
- console.log(`Rotated MCP password: ${args.noPrintCredentials ? "<redacted>" : state.worker.oauthPassword}`);
700
- console.log(`Rotated daemon secret: ${args.noPrintCredentials ? "<redacted>" : previewSecret(state.worker.daemonSecret)}`);
701
- console.log("Run start to redeploy the Worker with the new secrets and revoke old OAuth access tokens.");
771
+ const showMcpPassword = Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials);
772
+ console.log(`Rotated MCP connection password: ${showMcpPassword ? state.worker.oauthPassword : previewSecret(state.worker.oauthPassword)}`);
773
+ console.log(`Rotated daemon secret: ${previewSecret(state.worker.daemonSecret)}`);
774
+ console.log("Run `machine-mcp --print-mcp-credentials` to redeploy and display the new client connection credentials when needed.");
702
775
  } finally {
703
776
  startupLock.release();
704
777
  }
@@ -742,23 +815,23 @@ async function serviceCommand(args) {
742
815
  throw new Error(`Unknown service action: ${action}`);
743
816
  }
744
817
 
745
- async function installAutostartBestEffort({ workspace, stateRoot, entryScript }) {
818
+ async function installAutostartBestEffort({ workspace, stateRoot, entryScript, logger }) {
746
819
  try {
747
820
  const { installAutostart } = await import("./service.mjs");
748
- const result = await installAutostart({ workspace, stateRoot, entryScript, logger: structuredLogger(false) });
749
- if (result?.ok) console.log("Autostart installed for future logins. Use `machine-mcp service status` to inspect or `machine-mcp service uninstall` to remove.");
750
- else console.warn("Autostart installation returned a warning; run `machine-mcp service status` for details.");
821
+ const result = await installAutostart({ workspace, stateRoot, entryScript, logger: structuredLogger(true) });
822
+ if (result?.ok) logger.info("Autostart installed for future logins", { provider: result.provider });
823
+ else logger.warn("Autostart installation reported a problem; run `machine-mcp service status` for details");
751
824
  } catch (error) {
752
- console.warn(`Autostart installation skipped: ${error.message}`);
825
+ logger.warn("Autostart installation skipped", { error_class: classifyOperationalError(error) });
753
826
  }
754
827
  }
755
828
 
756
- async function stopAutostartBestEffort(quiet = false) {
829
+ async function stopAutostartBestEffort(logger) {
757
830
  try {
758
831
  const { stopAutostart } = await import("./service.mjs");
759
832
  await stopAutostart({ logger: structuredLogger(true) });
760
833
  } catch (error) {
761
- if (!quiet) console.warn(`Autostart stop skipped: ${error.message}`);
834
+ logger.warn("Autostart stop skipped", { error_class: classifyOperationalError(error) });
762
835
  }
763
836
  }
764
837
 
@@ -820,6 +893,7 @@ function knownWorkerNames(stateRoot) {
820
893
  const stateFile = resolve(profiles, entry.name, "state.json");
821
894
  if (!existsSync(stateFile)) continue;
822
895
  try {
896
+ if (statSync(stateFile).size > 2 * 1024 * 1024) continue;
823
897
  const state = JSON.parse(readFileSync(stateFile, "utf8"));
824
898
  const name = String(state?.worker?.name || "");
825
899
  if (/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(name)) names.add(name);
@@ -839,7 +913,7 @@ async function removeAutostartBestEffort(stateRoot) {
839
913
  }
840
914
  return true;
841
915
  } catch (error) {
842
- console.warn(`Autostart removal skipped or failed: ${error.message}`);
916
+ console.warn(`Autostart removal skipped or failed (${classifyOperationalError(error)}). Run machine-mcp service status for details.`);
843
917
  return false;
844
918
  }
845
919
  }
@@ -872,9 +946,10 @@ function isPidAlive(pid) {
872
946
  }
873
947
 
874
948
  function structuredLogger(quiet) {
875
- return createLogger({ quiet, component: "daemon" });
949
+ return createLogger({ quiet, component: "service" });
876
950
  }
877
951
 
952
+
878
953
  function sanitizeLines(text) {
879
954
  const home = process.env.HOME || process.env.USERPROFILE || "";
880
955
  const homePattern = home ? new RegExp(escapeRegExp(home), "g") : null;
@@ -910,7 +985,7 @@ function usage() {
910
985
  console.log(`machine-bridge-mcp
911
986
 
912
987
  Usage:
913
- npm install -g machine-bridge-mcp@latest && machine-mcp
988
+ npm install -g --allow-scripts=esbuild,workerd,sharp machine-bridge-mcp@latest && machine-mcp
914
989
  npx machine-bridge-mcp@latest # no global install; autostart may rely on npm cache
915
990
  ./mbm # from source checkout
916
991
  .\\mbm.cmd # from source checkout on Windows cmd
@@ -946,9 +1021,12 @@ Start options:
946
1021
  --no-exec Disable run_process and exec_command
947
1022
  --full-env Pass the full parent environment to local commands
948
1023
  --unrestricted-paths Allow filesystem tools outside the workspace
949
- --absolute-paths Return absolute local paths in tool results (off by default)
1024
+ --absolute-paths Return absolute local paths (enabled by the full profile)
950
1025
  --state-dir DIR Override state root
951
- --json Print MCP connection details as JSON
1026
+ --json Print connection details as JSON; credentials stay redacted unless explicitly requested
1027
+ --log-level LEVEL error, warn, info (default), or debug
1028
+ --verbose Alias for --log-level debug; includes per-tool success/correlation logs
1029
+ --quiet Alias for --log-level error
952
1030
 
953
1031
  Uninstall options:
954
1032
  --keep-worker Do not delete deployed Worker(s) during uninstall