machine-bridge-mcp 0.14.0 → 0.16.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +18 -8
  3. package/SECURITY.md +10 -3
  4. package/browser-extension/browser-operations.js +515 -0
  5. package/browser-extension/devtools-input.js +17 -2
  6. package/browser-extension/manifest.json +2 -2
  7. package/browser-extension/page-automation.js +245 -60
  8. package/browser-extension/pairing.js +1 -1
  9. package/browser-extension/service-worker.js +151 -354
  10. package/docs/ARCHITECTURE.md +6 -4
  11. package/docs/AUDIT.md +24 -1
  12. package/docs/LOCAL_AUTOMATION.md +10 -10
  13. package/docs/LOGGING.md +7 -1
  14. package/docs/OPERATIONS.md +13 -5
  15. package/docs/POLICY_REFERENCE.md +88 -0
  16. package/docs/TESTING.md +9 -2
  17. package/package.json +16 -3
  18. package/scripts/coverage-check.mjs +108 -0
  19. package/scripts/generate-policy-reference.mjs +95 -0
  20. package/src/local/agent-context.mjs +1 -103
  21. package/src/local/app-automation.mjs +7 -9
  22. package/src/local/browser-bridge.mjs +69 -143
  23. package/src/local/browser-extension-protocol.mjs +75 -0
  24. package/src/local/browser-pairing-store.mjs +83 -0
  25. package/src/local/call-registry.mjs +113 -0
  26. package/src/local/capability-ranking.mjs +103 -0
  27. package/src/local/cli-local-admin.mjs +308 -0
  28. package/src/local/cli-options.mjs +151 -0
  29. package/src/local/cli-policy.mjs +77 -0
  30. package/src/local/cli.mjs +16 -507
  31. package/src/local/errors.mjs +122 -0
  32. package/src/local/git-service.mjs +88 -0
  33. package/src/local/lifecycle.mjs +64 -0
  34. package/src/local/log.mjs +50 -19
  35. package/src/local/managed-job-plan.mjs +235 -0
  36. package/src/local/managed-jobs.mjs +16 -220
  37. package/src/local/observability.mjs +83 -0
  38. package/src/local/policy.mjs +148 -0
  39. package/src/local/process-execution.mjs +153 -0
  40. package/src/local/process-sessions.mjs +10 -20
  41. package/src/local/process-tracker.mjs +55 -0
  42. package/src/local/runtime.mjs +154 -672
  43. package/src/local/service.mjs +8 -3
  44. package/src/local/stdio.mjs +3 -11
  45. package/src/local/tool-executor.mjs +102 -0
  46. package/src/local/tools.mjs +21 -104
  47. package/src/local/workspace-file-service.mjs +451 -0
  48. package/src/shared/policy-contract.json +54 -0
  49. package/src/shared/tool-catalog.json +11 -11
  50. package/src/worker/index.ts +69 -524
package/src/local/cli.mjs CHANGED
@@ -1,4 +1,3 @@
1
- import { spawn } from "node:child_process";
2
1
  import { createHash, randomBytes } from "node:crypto";
3
2
  import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
4
3
  import path, { join, resolve } from "node:path";
@@ -7,11 +6,15 @@ import readline from "node:readline/promises";
7
6
  import { LocalRuntime } from "./runtime.mjs";
8
7
  import { acquireDaemonLockWithTakeover, inspectWorkspaceDaemon, stopWorkspaceServiceDaemon } from "./daemon-process.mjs";
9
8
  import { runStdioServer } from "./stdio.mjs";
10
- import { assertCanonicalFullPolicy, DEFAULT_POLICY_PROFILE, DEFAULT_POLICY_REVISION, POLICY_PROFILES, normalizePolicy, policyProfile, toolsForPolicy } from "./tools.mjs";
9
+ import { assertCanonicalFullPolicy, POLICY_PROFILES, toolsForPolicy } from "./tools.mjs";
10
+ import { resolvePolicy } from "./cli-policy.mjs";
11
+ import { effectiveLogFormat, effectiveLogLevel, normalizeCommand, parseArgs, validateCommandOptions, validateLoggingOptions, validatePositionals } from "./cli-options.mjs";
12
+ import { createLocalAdminCommands } from "./cli-local-admin.mjs";
13
+ export { resolvePolicy } from "./cli-policy.mjs";
14
+ export { parseArgs, validateCommandOptions, validateLoggingOptions, validatePositionals } from "./cli-options.mjs";
11
15
  import { classifyOperationalError, createLogger, normalizeLogLevel, sanitizeLogText } from "./log.mjs";
12
- import { activeManagedJobs, inspectResourceFile, loadManagedJobPlan, ManagedJobManager, publicResourceRegistry, validateResourceName } from "./managed-jobs.mjs";
16
+ import { activeManagedJobs } from "./managed-jobs.mjs";
13
17
  import { run, runWrangler } from "./shell.mjs";
14
- import { generateRegisteredSshKey } from "./resource-operations.mjs";
15
18
  import { runFullAccessTest } from "./full-access-test.mjs";
16
19
  import { readBoundedRegularFileSync } from "./secure-file.mjs";
17
20
  import { inspectProcessInstance } from "./process-identity.mjs";
@@ -42,15 +45,7 @@ import {
42
45
  setSelectedWorkspace,
43
46
  } from "./state.mjs";
44
47
 
45
- const BOOLEAN_OPTIONS = new Set([
46
- "help", "version", "quiet", "json", "verbose", "rotateSecrets", "forceWorker",
47
- "daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials",
48
- "printCredentials", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
49
- "yes", "keepWorker", "allowInsecurePermissions", "showPaths",
50
- ]);
51
- const VALUE_OPTIONS = new Set([
52
- "workspace", "stateDir", "workerName", "profile", "execMode", "client", "logLevel",
53
- ]);
48
+ const localAdminCommands = createLocalAdminCommands({ chooseWorkspace, confirm });
54
49
 
55
50
  const COMMAND_HANDLERS = Object.freeze({
56
51
  start: startCommand,
@@ -63,9 +58,9 @@ const COMMAND_HANDLERS = Object.freeze({
63
58
  service: serviceCommand,
64
59
  autostart: serviceCommand,
65
60
  "rotate-secrets": rotateSecretsCommand,
66
- resource: resourceCommand,
67
- browser: browserCommand,
68
- job: jobCommand,
61
+ resource: localAdminCommands.resourceCommand,
62
+ browser: localAdminCommands.browserCommand,
63
+ job: localAdminCommands.jobCommand,
69
64
  uninstall: uninstallCommand,
70
65
  });
71
66
 
@@ -85,155 +80,6 @@ export async function main(argv = process.argv.slice(2)) {
85
80
  process.exitCode = 2;
86
81
  }
87
82
 
88
- const COMMAND_OPTIONS = {
89
- start: new Set([
90
- "workspace", "stateDir", "workerName", "quiet", "json", "verbose", "logLevel", "rotateSecrets", "forceWorker",
91
- "daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials", "printCredentials",
92
- "profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
93
- ]),
94
- stdio: new Set(["workspace", "stateDir", "profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths", "verbose", "quiet", "logLevel"]),
95
- "client-config": new Set(["workspace", "stateDir", "profile", "client", "json"]),
96
- status: new Set(["workspace", "stateDir"]),
97
- doctor: new Set(["workspace", "stateDir"]),
98
- "full-test": new Set(["workspace", "stateDir", "json"]),
99
- "rotate-secrets": new Set(["workspace", "stateDir", "workerName", "noPrintCredentials", "printMcpCredentials", "printCredentials", "quiet"]),
100
- workspace: new Set(["workspace", "stateDir"]),
101
- service: new Set(["workspace", "stateDir", "quiet"]),
102
- autostart: new Set(["workspace", "stateDir", "quiet"]),
103
- resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "showPaths", "json"]),
104
- browser: new Set(["workspace", "stateDir", "json"]),
105
- job: new Set(["workspace", "stateDir", "json", "yes"]),
106
- uninstall: new Set(["stateDir", "keepWorker", "yes"]),
107
- };
108
-
109
- export function validateCommandOptions(command, args) {
110
- const allowed = COMMAND_OPTIONS[command];
111
- if (!allowed) return;
112
- for (const key of Object.keys(args)) {
113
- if (key === "_" || key === "help" || key === "version") continue;
114
- if (!allowed.has(key)) throw new Error(`Option --${toKebab(key)} is not valid for ${command}`);
115
- }
116
- }
117
-
118
- export function validateLoggingOptions(args = {}) {
119
- if (args.quiet && args.verbose) throw new Error("--quiet and --verbose cannot be used together");
120
- if (args.logLevel !== undefined && (args.quiet || args.verbose)) {
121
- throw new Error("--log-level cannot be combined with --quiet or --verbose");
122
- }
123
- if (args.logLevel !== undefined) normalizeLogLevel(args.logLevel);
124
- }
125
-
126
- function effectiveLogLevel(args = {}) {
127
- if (args.logLevel !== undefined) return normalizeLogLevel(args.logLevel);
128
- if (args.quiet) return "error";
129
- if (args.verbose) return "debug";
130
- return "info";
131
- }
132
-
133
- function toKebab(value) {
134
- return String(value).replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
135
- }
136
-
137
- const SINGLE_WORKSPACE_POSITIONAL_COMMANDS = new Set(["start", "stdio", "status", "doctor", "full-test", "rotate-secrets"]);
138
- const STATIC_POSITIONAL_RULES = Object.freeze({
139
- "client-config": Object.freeze({ max: 1, tooMany: "client-config accepts at most one positional client name" }),
140
- uninstall: Object.freeze({ max: 0, tooMany: "uninstall does not accept positional arguments" }),
141
- });
142
- const RESOURCE_POSITIONAL_LIMITS = Object.freeze({ add: 3, "generate-ssh-key": 3, remove: 2, check: 2 });
143
- const JOB_POSITIONAL_LIMITS = Object.freeze({ read: 2, inspect: 2, cancel: 2, approve: 2, submit: 2 });
144
- const ACTION_POSITIONAL_RULES = Object.freeze({
145
- workspace(args) {
146
- const action = String(args._[0] || "show");
147
- return { max: action === "set" || action === "select" ? 2 : 1, tooMany: `workspace ${action} received too many positional arguments`, workspaceConflictAfter: 1 };
148
- },
149
- service(args) {
150
- const action = String(args._[0] || "status");
151
- return { max: ["install", "status", "stop", "uninstall", "remove"].includes(action) ? 2 : 1, tooMany: `service ${action} received too many positional arguments`, workspaceConflictAfter: 1 };
152
- },
153
- autostart(args) {
154
- return ACTION_POSITIONAL_RULES.service(args);
155
- },
156
- resource(args) {
157
- const action = String(args._[0] || "list");
158
- return { max: RESOURCE_POSITIONAL_LIMITS[action] ?? 1, tooMany: `resource ${action} received too many positional arguments` };
159
- },
160
- browser(args) {
161
- const action = String(args._[0] || "status");
162
- return { max: 1, tooMany: `browser ${action} received too many positional arguments` };
163
- },
164
- job(args) {
165
- const action = String(args._[0] || "list");
166
- return { max: JOB_POSITIONAL_LIMITS[action] ?? 1, tooMany: `job ${action} received too many positional arguments` };
167
- },
168
- });
169
-
170
- export function validatePositionals(command, args) {
171
- const count = args._.length;
172
- const rule = positionalRule(command, args);
173
- if (!rule) return;
174
- if (count > rule.max) throw new Error(rule.tooMany);
175
- if (args.workspace && Number.isInteger(rule.workspaceConflictAfter) && count > rule.workspaceConflictAfter) {
176
- throw new Error("workspace path was provided both positionally and with --workspace");
177
- }
178
- }
179
-
180
- function positionalRule(command, args) {
181
- if (SINGLE_WORKSPACE_POSITIONAL_COMMANDS.has(command)) {
182
- return {
183
- max: 1,
184
- tooMany: `${command} accepts at most one positional workspace path`,
185
- workspaceConflictAfter: 0,
186
- };
187
- }
188
- if (STATIC_POSITIONAL_RULES[command]) return STATIC_POSITIONAL_RULES[command];
189
- return ACTION_POSITIONAL_RULES[command]?.(args) || null;
190
- }
191
-
192
- function normalizeCommand(argv) {
193
- if (!argv.length || argv[0].startsWith("--")) return ["start", argv];
194
- return [argv[0], argv.slice(1)];
195
- }
196
-
197
- export function parseArgs(argv) {
198
- const out = { _: [] };
199
- let positionalOnly = false;
200
- for (let i = 0; i < argv.length; i += 1) {
201
- const raw = argv[i];
202
- if (positionalOnly || raw === "-" || !raw.startsWith("--")) {
203
- out._.push(raw);
204
- continue;
205
- }
206
- if (raw === "--") {
207
- positionalOnly = true;
208
- continue;
209
- }
210
- const eq = raw.indexOf("=");
211
- const rawKey = raw.slice(2, eq >= 0 ? eq : undefined);
212
- if (!rawKey) throw new Error("invalid empty option");
213
- const key = toCamel(rawKey);
214
- if (!BOOLEAN_OPTIONS.has(key) && !VALUE_OPTIONS.has(key)) throw new Error(`Unknown option: --${rawKey}`);
215
- if (Object.prototype.hasOwnProperty.call(out, key)) throw new Error(`Duplicate option: --${rawKey}`);
216
- if (BOOLEAN_OPTIONS.has(key)) {
217
- out[key] = eq >= 0 ? parseBooleanOption(raw.slice(eq + 1), rawKey) : true;
218
- continue;
219
- }
220
- const value = eq >= 0 ? raw.slice(eq + 1) : argv[++i];
221
- if (value === undefined || value === "" || value.startsWith("--")) throw new Error(`Option --${rawKey} requires a value`);
222
- out[key] = value;
223
- }
224
- return out;
225
- }
226
-
227
- function parseBooleanOption(value, key) {
228
- if (value === "true" || value === "1") return true;
229
- if (value === "false" || value === "0") return false;
230
- throw new Error(`Option --${key} expects true or false when using =`);
231
- }
232
-
233
- function toCamel(key) {
234
- return key.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
235
- }
236
-
237
83
  function stateRootFromArgs(args) {
238
84
  return args.stateDir ? expandHome(String(args.stateDir)) : defaultStateRoot();
239
85
  }
@@ -314,7 +160,7 @@ async function confirm(prompt, assumeYes = false) {
314
160
 
315
161
  async function startCommand(args) {
316
162
  assertNodeVersion();
317
- const logger = createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "cli" });
163
+ const logger = createLogger({ level: args.json ? "error" : effectiveLogLevel(args), format: effectiveLogFormat(args), component: "cli" });
318
164
  const workspace = await chooseWorkspace(args, { promptOnFirstRun: true, save: true, allowPositional: true });
319
165
  const state = loadState(workspace, { stateDir: args.stateDir });
320
166
  const startupLock = await acquireStartupLockWithWait(state, { operation: "start", logger });
@@ -442,7 +288,7 @@ function createRemoteRuntime({ args, workspace, state, daemonLock, logger }) {
442
288
  expectedRelayVersion: currentPackageVersion(),
443
289
  workspace,
444
290
  policy: state.policy,
445
- logger: createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "daemon" }),
291
+ logger: createLogger({ level: args.json ? "error" : effectiveLogLevel(args), format: effectiveLogFormat(args), component: "daemon" }),
446
292
  jobRoot: join(state.paths.profileDir, "jobs"),
447
293
  resources: state.resources,
448
294
  resourceStatePath: state.paths.statePath,
@@ -475,77 +321,6 @@ function reportRemoteReady(args, state, readiness) {
475
321
  });
476
322
  }
477
323
 
478
- export function resolvePolicy(args = {}, stored = {}) {
479
- const hasStored = stored && typeof stored === "object" && (
480
- typeof stored.allowWrite === "boolean" || typeof stored.allowExec === "boolean" || typeof stored.execMode === "string"
481
- );
482
- const explicitKeys = ["profile", ...POLICY_OVERRIDE_KEYS];
483
- const hasExplicit = explicitKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key));
484
- const base = selectPolicyBase(args, stored, hasStored);
485
- if (!hasExplicit) return normalizePolicy(base);
486
- applyPolicyOverrides(base, args);
487
- if (args.profile === undefined || POLICY_OVERRIDE_KEYS.some((key) => Object.prototype.hasOwnProperty.call(args, key))) {
488
- base.profile = "custom";
489
- base.origin = "custom";
490
- base.revision = DEFAULT_POLICY_REVISION;
491
- }
492
- return normalizePolicy(base);
493
- }
494
-
495
- const POLICY_OVERRIDE_KEYS = Object.freeze(["execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths"]);
496
-
497
- function selectPolicyBase(args, stored, hasStored) {
498
- if (args.profile !== undefined) {
499
- const profile = String(args.profile).trim().toLowerCase();
500
- if (!POLICY_PROFILES[profile]) throw new Error(`--profile must be one of: ${Object.keys(POLICY_PROFILES).join(", ")}`);
501
- return policyProfile(profile, "explicit");
502
- }
503
- if (hasStored) return migrateLegacyPolicy(stored);
504
- return policyProfile(DEFAULT_POLICY_PROFILE, "default");
505
- }
506
-
507
- function applyPolicyOverrides(policy, args) {
508
- if (args.execMode !== undefined) {
509
- const execMode = String(args.execMode).trim().toLowerCase();
510
- if (!["off", "direct", "shell"].includes(execMode)) throw new Error("--exec-mode must be off, direct, or shell");
511
- policy.execMode = execMode;
512
- }
513
- applyBooleanOverride(args, "noWrite", (enabled) => { policy.allowWrite = !enabled; });
514
- applyBooleanOverride(args, "noExec", (enabled) => {
515
- if (enabled) policy.execMode = "off";
516
- else if (policy.execMode === "off") policy.execMode = "direct";
517
- });
518
- applyBooleanOverride(args, "fullEnv", (enabled) => { policy.minimalEnv = !enabled; });
519
- applyBooleanOverride(args, "unrestrictedPaths", (enabled) => { policy.unrestrictedPaths = enabled; });
520
- applyBooleanOverride(args, "absolutePaths", (enabled) => { policy.exposeAbsolutePaths = enabled; });
521
- }
522
-
523
- function applyBooleanOverride(args, key, apply) {
524
- if (typeof args[key] === "boolean") apply(args[key]);
525
- }
526
-
527
- function migrateLegacyPolicy(stored = {}) {
528
- if (stored.origin === "default" && Number(stored.revision || 0) < DEFAULT_POLICY_REVISION) {
529
- return policyProfile(DEFAULT_POLICY_PROFILE, "default");
530
- }
531
- if (stored.origin === "migrated" && Number(stored.revision || 0) < DEFAULT_POLICY_REVISION) {
532
- return policyProfile(DEFAULT_POLICY_PROFILE, "migrated");
533
- }
534
- if (stored.origin) return normalizePolicy(stored);
535
- const normalized = normalizePolicy(stored);
536
- const looksLikeLegacyImplicitDefault = (
537
- normalized.profile === "custom" &&
538
- normalized.allowWrite === true &&
539
- normalized.execMode === "shell" &&
540
- normalized.unrestrictedPaths === false &&
541
- normalized.minimalEnv === true &&
542
- normalized.exposeAbsolutePaths === false
543
- );
544
- if (looksLikeLegacyImplicitDefault) return policyProfile("full", "migrated");
545
- return normalizePolicy({ ...normalized, origin: "legacy-preserved", revision: DEFAULT_POLICY_REVISION });
546
- }
547
-
548
-
549
324
  async function stdioCommand(args) {
550
325
  assertNodeVersion();
551
326
  const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
@@ -555,6 +330,7 @@ async function stdioCommand(args) {
555
330
  workspace,
556
331
  policy,
557
332
  logLevel: effectiveLogLevel(args),
333
+ logFormat: effectiveLogFormat(args),
558
334
  jobRoot: join(state.paths.profileDir, "jobs"),
559
335
  resources: state.resources,
560
336
  resourceStatePath: state.paths.statePath,
@@ -562,274 +338,6 @@ async function stdioCommand(args) {
562
338
  });
563
339
  }
564
340
 
565
- const RESOURCE_ACTION_HANDLERS = Object.freeze({
566
- list: resourceListAction,
567
- add: resourceAddAction,
568
- "generate-ssh-key": resourceGenerateSshKeyAction,
569
- remove: resourceRemoveAction,
570
- check: resourceCheckAction,
571
- });
572
-
573
- async function resourceCommand(args) {
574
- const action = String(args._[0] || "list").toLowerCase();
575
- const handler = RESOURCE_ACTION_HANDLERS[action];
576
- if (!handler) throw new Error(`Unknown resource action: ${action}`);
577
- const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
578
- const state = loadState(workspace, { stateDir: args.stateDir });
579
- state.resources ||= {};
580
- return handler({ args, workspace, state });
581
- }
582
-
583
- function resourceListAction({ args, workspace, state }) {
584
- const includePaths = args.showPaths === true;
585
- const resources = publicResourceRegistry(state.resources, { includePaths });
586
- if (args.json) {
587
- console.log(JSON.stringify({
588
- workspace: includePaths ? workspace : "<local-workspace>",
589
- paths_exposed: includePaths,
590
- resources,
591
- }, null, 2));
592
- return;
593
- }
594
- if (!Object.keys(resources).length) {
595
- console.log("No local resources registered.");
596
- return;
597
- }
598
- for (const [name, value] of Object.entries(resources)) {
599
- const fields = [name, value.mode || "n/a", `${value.size ?? "n/a"} bytes`];
600
- if (includePaths) fields.splice(1, 0, value.path);
601
- console.log(fields.join(" "));
602
- }
603
- }
604
-
605
- async function resourceAddAction({ args, workspace, state }) {
606
- const name = validateResourceName(args._[1]);
607
- const inputPath = args._[2];
608
- if (!inputPath) throw new Error("resource add requires NAME and FILE_PATH");
609
- const lock = await acquireStartupLockWithWait(state, { operation: "resource-add" });
610
- try {
611
- const latest = loadState(workspace, { stateDir: args.stateDir });
612
- latest.resources ||= {};
613
- const inspected = inspectResourceFile(expandHome(inputPath), { allowInsecurePermissions: args.allowInsecurePermissions === true });
614
- if (!Object.prototype.hasOwnProperty.call(latest.resources, name) && Object.keys(latest.resources).length >= 64) {
615
- throw new Error("local resource registry limit reached (64)");
616
- }
617
- latest.resources[name] = inspected;
618
- saveState(latest);
619
- const result = publicResourceInspection(name, inspected, {
620
- includePath: args.showPaths === true,
621
- available_to_new_jobs_immediately: true,
622
- });
623
- if (args.json) console.log(JSON.stringify(result, null, 2));
624
- else {
625
- console.log(`Registered local resource: ${name}`);
626
- if (args.showPaths === true) console.log(`Path: ${inspected.path}`);
627
- console.log(`Mode: ${inspected.mode || "n/a"}; size: ${inspected.size} bytes`);
628
- console.log("The resource is available to newly submitted managed jobs immediately.");
629
- }
630
- } finally {
631
- lock.release();
632
- }
633
- }
634
-
635
- async function resourceGenerateSshKeyAction({ args, workspace }) {
636
- const name = validateResourceName(args._[1]);
637
- const home = process.env.HOME || process.env.USERPROFILE;
638
- if (!home) throw new Error("HOME or USERPROFILE is required to choose a default SSH key path");
639
- const requestedPath = args._[2] ? expandHome(args._[2]) : join(home, ".ssh", `machine-mcp-${name}-ed25519`);
640
- const key = await generateRegisteredSshKey({
641
- workspace,
642
- stateDir: args.stateDir,
643
- name,
644
- targetPath: requestedPath,
645
- comment: `machine-mcp:${name}`,
646
- });
647
- const includePaths = args.showPaths === true;
648
- const result = {
649
- name: key.name,
650
- created: key.created,
651
- fingerprint: key.fingerprint,
652
- key_type: key.keyType,
653
- private_mode: key.privateMode,
654
- public_mode: key.publicMode,
655
- private_key_content_exposed: key.privateKeyContentExposed,
656
- registered: key.registered,
657
- available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
658
- paths_exposed: includePaths,
659
- ...(includePaths ? { private_key_path: key.privateKeyPath, public_key_path: key.publicKeyPath } : {}),
660
- };
661
- if (args.json) console.log(JSON.stringify(result, null, 2));
662
- else {
663
- console.log(`${key.created ? "Generated and registered" : "Reused and registered"} SSH key resource: ${name}`);
664
- if (includePaths) {
665
- console.log(`Private key: ${key.privateKeyPath}`);
666
- console.log(`Public key: ${key.publicKeyPath}`);
667
- }
668
- console.log(`Fingerprint: ${key.fingerprint}`);
669
- console.log("Private key content was not printed or sent through MCP.");
670
- }
671
- }
672
-
673
- async function resourceRemoveAction({ args, workspace, state }) {
674
- const name = validateResourceName(args._[1]);
675
- const lock = await acquireStartupLockWithWait(state, { operation: "resource-remove" });
676
- try {
677
- const latest = loadState(workspace, { stateDir: args.stateDir });
678
- latest.resources ||= {};
679
- const existed = Object.prototype.hasOwnProperty.call(latest.resources, name);
680
- delete latest.resources[name];
681
- saveState(latest);
682
- const result = { name, removed: existed, affects_new_jobs_immediately: true };
683
- if (args.json) console.log(JSON.stringify(result, null, 2));
684
- else {
685
- console.log(existed ? `Removed local resource: ${name}` : `Local resource was not registered: ${name}`);
686
- console.log("The change applies to newly submitted managed jobs immediately.");
687
- }
688
- } finally {
689
- lock.release();
690
- }
691
- }
692
-
693
- function resourceCheckAction({ args, state }) {
694
- const name = validateResourceName(args._[1]);
695
- const resource = state.resources[name];
696
- if (!resource) throw new Error(`local resource is not registered: ${name}`);
697
- const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true });
698
- const result = publicResourceInspection(name, inspected, { includePath: args.showPaths === true });
699
- if (args.json) console.log(JSON.stringify(result, null, 2));
700
- else {
701
- const pathDetail = args.showPaths === true ? ` at ${inspected.path}` : "";
702
- console.log(`${name}: available${pathDetail} (${inspected.mode || "n/a"}, ${inspected.size} bytes)`);
703
- }
704
- }
705
-
706
- function publicResourceInspection(name, inspected, { includePath = false, ...extra } = {}) {
707
- return {
708
- name,
709
- kind: inspected.kind,
710
- size: inspected.size ?? null,
711
- mode: inspected.mode ?? null,
712
- updated_at: inspected.updatedAt ?? null,
713
- allow_insecure_permissions: inspected.allowInsecurePermissions === true,
714
- ...extra,
715
- paths_exposed: includePath,
716
- contents_exposed: false,
717
- ...(includePath ? { path: inspected.path } : {}),
718
- };
719
- }
720
-
721
- async function browserCommand(args) {
722
- const action = String(args._[0] || "status").toLowerCase();
723
- const extensionPath = resolve(packageRoot, "browser-extension");
724
- if (action === "path") {
725
- if (args.json) console.log(JSON.stringify({ extension_path: extensionPath }, null, 2));
726
- else console.log(extensionPath);
727
- return;
728
- }
729
- if (!["status", "setup", "pair"].includes(action)) throw new Error(`Unknown browser action: ${action}`);
730
- const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
731
- const state = loadState(workspace, { stateDir: args.stateDir });
732
- const pairingFile = join(state.paths.stateRoot, "browser-bridge.json");
733
- if (!existsSync(pairingFile)) {
734
- throw new Error("browser bridge is not initialized; start machine-mcp once, then run this command again");
735
- }
736
- ownerOnlyFile(pairingFile);
737
- let pairing;
738
- try {
739
- pairing = JSON.parse(readBoundedRegularFileSync(pairingFile, 64 * 1024).toString("utf8"));
740
- } catch {
741
- throw new Error("browser bridge state is invalid; restart machine-mcp to repair it");
742
- }
743
- const port = Number(pairing.port);
744
- if (!/^[A-Za-z0-9_-]{32,100}$/.test(String(pairing.token || ""))) throw new Error("browser bridge state contains an invalid token");
745
- if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("browser bridge state contains an invalid port");
746
- const pairingUrl = `http://127.0.0.1:${port}/pair`;
747
- const healthUrl = `http://127.0.0.1:${port}/healthz`;
748
- const health = await fetch(healthUrl, { signal: AbortSignal.timeout(2000), cache: "no-store" })
749
- .then(async (response) => response.ok ? await response.json() : null)
750
- .catch(() => null);
751
- const result = {
752
- running: health?.ok === true && health?.broker === "machine-bridge-browser",
753
- connected: health?.broker === "machine-bridge-browser" && health?.connected === true,
754
- extension_path: extensionPath,
755
- pairing_url: pairingUrl,
756
- token_exposed: false,
757
- };
758
- if (action === "status") {
759
- if (args.json) console.log(JSON.stringify(result, null, 2));
760
- else {
761
- console.log(`Browser bridge: ${result.running ? "running" : "not reachable"}`);
762
- console.log(`Extension: ${result.connected ? "connected" : "not connected"}`);
763
- console.log(`Extension path: ${extensionPath}`);
764
- }
765
- return;
766
- }
767
- if (!result.running) throw new Error("browser bridge is not reachable; keep machine-mcp running and retry");
768
- await openExternal(pairingUrl);
769
- if (args.json) console.log(JSON.stringify({ ...result, pairing_page_opened: true }, null, 2));
770
- else {
771
- console.log(`Extension path: ${extensionPath}`);
772
- console.log("Load this directory once from the Chromium extensions page with Developer mode enabled.");
773
- console.log(`Pairing page opened: ${pairingUrl}`);
774
- }
775
- }
776
-
777
- function openExternal(target) {
778
- const command = process.platform === "darwin"
779
- ? { file: "open", args: [target] }
780
- : process.platform === "win32"
781
- ? { file: "cmd.exe", args: ["/d", "/s", "/c", "start", "", target] }
782
- : { file: "xdg-open", args: [target] };
783
- return new Promise((resolvePromise, rejectPromise) => {
784
- const child = spawn(command.file, command.args, { detached: true, stdio: "ignore", windowsHide: true });
785
- child.once("spawn", () => {
786
- child.unref();
787
- resolvePromise();
788
- });
789
- child.once("error", rejectPromise);
790
- });
791
- }
792
-
793
- async function jobCommand(args) {
794
- const action = String(args._[0] || "list").toLowerCase();
795
- const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
796
- const state = loadState(workspace, { stateDir: args.stateDir });
797
- const manager = new ManagedJobManager({
798
- jobRoot: join(state.paths.profileDir, "jobs"),
799
- workspace,
800
- policy: resolvePolicy({}, state.policy),
801
- resources: state.resources,
802
- resourceStatePath: state.paths.statePath,
803
- stateRoot: state.paths.stateRoot,
804
- logger: createLogger({ level: "warn", component: "job" }),
805
- });
806
- let result;
807
- if (action === "list") result = manager.list({ limit: 50 });
808
- else if (action === "read") result = manager.read({ job_id: args._[1] });
809
- else if (action === "inspect") result = manager.inspectLocal({ job_id: args._[1] });
810
- else if (action === "cancel") result = manager.cancel({ job_id: args._[1] });
811
- else if (action === "approve") {
812
- if (args.json && !args.yes) throw new Error("job approve --json requires --yes");
813
- const inspection = manager.inspectLocal({ job_id: args._[1] });
814
- if (!args.yes) {
815
- console.log(JSON.stringify(inspection, null, 2));
816
- const approved = await confirm(`Approve and execute managed job ${args._[1]}?`, false);
817
- if (!approved) {
818
- console.log("Managed job approval cancelled. Re-run with --yes after review to skip confirmation.");
819
- return;
820
- }
821
- }
822
- result = manager.approve({ job_id: args._[1] }, { localOperator: true });
823
- }
824
- else if (action === "submit") {
825
- const planPath = args._[1];
826
- if (!planPath) throw new Error("job submit requires a JSON plan file");
827
- const plan = loadManagedJobPlan(expandHome(planPath));
828
- result = manager.start(plan);
829
- } else throw new Error(`Unknown job action: ${action}`);
830
- console.log(JSON.stringify(result, null, 2));
831
- }
832
-
833
341
  async function clientConfigCommand(args) {
834
342
  const workspaceArgs = { ...args, _: [] };
835
343
  const workspace = await chooseWorkspace(workspaceArgs, { promptOnFirstRun: false, save: false, allowPositional: false });
@@ -862,7 +370,7 @@ function removedLocalApiError() {
862
370
 
863
371
 
864
372
  async function ensureWorker(state, args) {
865
- const logger = createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "worker" });
373
+ const logger = createLogger({ level: args.json ? "error" : effectiveLogLevel(args), format: effectiveLogFormat(args), component: "worker" });
866
374
  const desiredHash = workerDeployHash(state);
867
375
  const expectedVersion = currentPackageVersion();
868
376
  const complete = state.worker.url && state.worker.mcpServerUrl && state.worker.oauthPassword && state.worker.daemonSecret && state.worker.oauthTokenVersion && state.worker.name;
@@ -1633,6 +1141,7 @@ Start options:
1633
1141
  --state-dir DIR Override state root
1634
1142
  --json Print connection details as JSON; credentials stay redacted unless explicitly requested
1635
1143
  --log-level LEVEL error, warn, info (default), or debug
1144
+ --log-format FORMAT text (default) or newline-delimited json
1636
1145
  --verbose Alias for --log-level debug; includes per-tool success/correlation logs
1637
1146
  --quiet Alias for --log-level error
1638
1147
  --allow-insecure-permissions