machine-bridge-mcp 0.15.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +12 -2
- package/SECURITY.md +7 -0
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +5 -3
- package/docs/AUDIT.md +12 -1
- package/docs/LOGGING.md +7 -1
- package/docs/OPERATIONS.md +9 -2
- package/docs/POLICY_REFERENCE.md +88 -0
- package/docs/TESTING.md +7 -0
- package/package.json +14 -3
- package/scripts/coverage-check.mjs +108 -0
- package/scripts/generate-policy-reference.mjs +95 -0
- package/src/local/agent-context.mjs +1 -103
- package/src/local/app-automation.mjs +7 -11
- package/src/local/browser-bridge.mjs +26 -156
- package/src/local/browser-extension-protocol.mjs +75 -0
- package/src/local/browser-pairing-store.mjs +83 -0
- package/src/local/call-registry.mjs +113 -0
- package/src/local/capability-ranking.mjs +103 -0
- package/src/local/cli-local-admin.mjs +308 -0
- package/src/local/cli-options.mjs +151 -0
- package/src/local/cli-policy.mjs +89 -0
- package/src/local/cli.mjs +16 -521
- package/src/local/errors.mjs +122 -0
- package/src/local/git-service.mjs +88 -0
- package/src/local/lifecycle.mjs +64 -0
- package/src/local/log.mjs +50 -19
- package/src/local/managed-job-plan.mjs +235 -0
- package/src/local/managed-jobs.mjs +16 -220
- package/src/local/observability.mjs +83 -0
- package/src/local/policy.mjs +148 -0
- package/src/local/process-execution.mjs +153 -0
- package/src/local/process-sessions.mjs +10 -20
- package/src/local/process-tracker.mjs +55 -0
- package/src/local/runtime.mjs +154 -672
- package/src/local/service.mjs +1 -0
- package/src/local/stdio.mjs +3 -11
- package/src/local/tool-executor.mjs +102 -0
- package/src/local/tools.mjs +21 -104
- package/src/local/workspace-file-service.mjs +451 -0
- package/src/shared/policy-contract.json +54 -0
- package/src/shared/tool-catalog.json +4 -4
- 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,
|
|
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
|
|
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
|
|
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,288 +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
|
-
expected_extension_version: typeof health?.expected_extension_version === "string" ? health.expected_extension_version : "",
|
|
757
|
-
extension_protocol: Number.isInteger(health?.extension_protocol) ? health.extension_protocol : null,
|
|
758
|
-
extension_version: typeof health?.extension_version === "string" ? health.extension_version : "",
|
|
759
|
-
extension_capabilities: Array.isArray(health?.extension_capabilities) ? health.extension_capabilities : [],
|
|
760
|
-
extension_reload_required: health?.extension_reload_required === true,
|
|
761
|
-
controls_existing_profile: health?.controls_existing_profile === true,
|
|
762
|
-
controls_extension_profile: health?.controls_extension_profile === true,
|
|
763
|
-
machine_bridge_launches_browser: health?.machine_bridge_launches_browser === true,
|
|
764
|
-
profile_identity_verifiable: health?.profile_identity_verifiable === true,
|
|
765
|
-
token_exposed: false,
|
|
766
|
-
};
|
|
767
|
-
if (action === "status") {
|
|
768
|
-
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
769
|
-
else {
|
|
770
|
-
console.log(`Browser bridge: ${result.running ? "running" : "not reachable"}`);
|
|
771
|
-
console.log(`Extension: ${result.connected ? "connected" : result.extension_reload_required ? "reload required" : "not connected"}`);
|
|
772
|
-
if (result.expected_extension_version) console.log(`Expected extension build: ${result.expected_extension_version}`);
|
|
773
|
-
if (result.extension_version || result.extension_protocol) console.log(`Connected extension build: ${result.extension_version || "unknown"} (protocol ${result.extension_protocol ?? "unknown"})`);
|
|
774
|
-
console.log(`Browser profile: ${result.controls_extension_profile ? "the Chromium profile where this extension is installed" : "unknown"}`);
|
|
775
|
-
if (result.controls_extension_profile) console.log(`Profile provenance: Machine Bridge did not launch the browser; daily-vs-isolated profile identity is not machine-verifiable.`);
|
|
776
|
-
console.log(`Extension path: ${extensionPath}`);
|
|
777
|
-
}
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
if (!result.running) throw new Error("browser bridge is not reachable; keep machine-mcp running and retry");
|
|
781
|
-
await openExternal(pairingUrl);
|
|
782
|
-
if (args.json) console.log(JSON.stringify({ ...result, pairing_page_opened: true }, null, 2));
|
|
783
|
-
else {
|
|
784
|
-
console.log(`Extension path: ${extensionPath}`);
|
|
785
|
-
console.log("Load this directory in the Chromium profile you use every day; Machine Bridge does not install it into Playwright or a separate automation profile.");
|
|
786
|
-
console.log("Enable Developer mode, choose Load unpacked, and reload the extension after each Machine Bridge upgrade.");
|
|
787
|
-
console.log(`Pairing page opened: ${pairingUrl}`);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
function openExternal(target) {
|
|
792
|
-
const command = process.platform === "darwin"
|
|
793
|
-
? { file: "open", args: [target] }
|
|
794
|
-
: process.platform === "win32"
|
|
795
|
-
? { file: "cmd.exe", args: ["/d", "/s", "/c", "start", "", target] }
|
|
796
|
-
: { file: "xdg-open", args: [target] };
|
|
797
|
-
return new Promise((resolvePromise, rejectPromise) => {
|
|
798
|
-
const child = spawn(command.file, command.args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
799
|
-
child.once("spawn", () => {
|
|
800
|
-
child.unref();
|
|
801
|
-
resolvePromise();
|
|
802
|
-
});
|
|
803
|
-
child.once("error", rejectPromise);
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
async function jobCommand(args) {
|
|
808
|
-
const action = String(args._[0] || "list").toLowerCase();
|
|
809
|
-
const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
|
|
810
|
-
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
811
|
-
const manager = new ManagedJobManager({
|
|
812
|
-
jobRoot: join(state.paths.profileDir, "jobs"),
|
|
813
|
-
workspace,
|
|
814
|
-
policy: resolvePolicy({}, state.policy),
|
|
815
|
-
resources: state.resources,
|
|
816
|
-
resourceStatePath: state.paths.statePath,
|
|
817
|
-
stateRoot: state.paths.stateRoot,
|
|
818
|
-
logger: createLogger({ level: "warn", component: "job" }),
|
|
819
|
-
});
|
|
820
|
-
let result;
|
|
821
|
-
if (action === "list") result = manager.list({ limit: 50 });
|
|
822
|
-
else if (action === "read") result = manager.read({ job_id: args._[1] });
|
|
823
|
-
else if (action === "inspect") result = manager.inspectLocal({ job_id: args._[1] });
|
|
824
|
-
else if (action === "cancel") result = manager.cancel({ job_id: args._[1] });
|
|
825
|
-
else if (action === "approve") {
|
|
826
|
-
if (args.json && !args.yes) throw new Error("job approve --json requires --yes");
|
|
827
|
-
const inspection = manager.inspectLocal({ job_id: args._[1] });
|
|
828
|
-
if (!args.yes) {
|
|
829
|
-
console.log(JSON.stringify(inspection, null, 2));
|
|
830
|
-
const approved = await confirm(`Approve and execute managed job ${args._[1]}?`, false);
|
|
831
|
-
if (!approved) {
|
|
832
|
-
console.log("Managed job approval cancelled. Re-run with --yes after review to skip confirmation.");
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
result = manager.approve({ job_id: args._[1] }, { localOperator: true });
|
|
837
|
-
}
|
|
838
|
-
else if (action === "submit") {
|
|
839
|
-
const planPath = args._[1];
|
|
840
|
-
if (!planPath) throw new Error("job submit requires a JSON plan file");
|
|
841
|
-
const plan = loadManagedJobPlan(expandHome(planPath));
|
|
842
|
-
result = manager.start(plan);
|
|
843
|
-
} else throw new Error(`Unknown job action: ${action}`);
|
|
844
|
-
console.log(JSON.stringify(result, null, 2));
|
|
845
|
-
}
|
|
846
|
-
|
|
847
341
|
async function clientConfigCommand(args) {
|
|
848
342
|
const workspaceArgs = { ...args, _: [] };
|
|
849
343
|
const workspace = await chooseWorkspace(workspaceArgs, { promptOnFirstRun: false, save: false, allowPositional: false });
|
|
@@ -876,7 +370,7 @@ function removedLocalApiError() {
|
|
|
876
370
|
|
|
877
371
|
|
|
878
372
|
async function ensureWorker(state, args) {
|
|
879
|
-
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" });
|
|
880
374
|
const desiredHash = workerDeployHash(state);
|
|
881
375
|
const expectedVersion = currentPackageVersion();
|
|
882
376
|
const complete = state.worker.url && state.worker.mcpServerUrl && state.worker.oauthPassword && state.worker.daemonSecret && state.worker.oauthTokenVersion && state.worker.name;
|
|
@@ -1647,6 +1141,7 @@ Start options:
|
|
|
1647
1141
|
--state-dir DIR Override state root
|
|
1648
1142
|
--json Print connection details as JSON; credentials stay redacted unless explicitly requested
|
|
1649
1143
|
--log-level LEVEL error, warn, info (default), or debug
|
|
1144
|
+
--log-format FORMAT text (default) or newline-delimited json
|
|
1650
1145
|
--verbose Alias for --log-level debug; includes per-tool success/correlation logs
|
|
1651
1146
|
--quiet Alias for --log-level error
|
|
1652
1147
|
--allow-insecure-permissions
|