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.
- package/CHANGELOG.md +39 -0
- package/README.md +18 -8
- package/SECURITY.md +10 -3
- package/browser-extension/browser-operations.js +515 -0
- package/browser-extension/devtools-input.js +17 -2
- package/browser-extension/manifest.json +2 -2
- package/browser-extension/page-automation.js +245 -60
- package/browser-extension/pairing.js +1 -1
- package/browser-extension/service-worker.js +151 -354
- package/docs/ARCHITECTURE.md +6 -4
- package/docs/AUDIT.md +24 -1
- package/docs/LOCAL_AUTOMATION.md +10 -10
- package/docs/LOGGING.md +7 -1
- package/docs/OPERATIONS.md +13 -5
- package/docs/POLICY_REFERENCE.md +88 -0
- package/docs/TESTING.md +9 -2
- package/package.json +16 -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 -9
- package/src/local/browser-bridge.mjs +69 -143
- 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 +77 -0
- package/src/local/cli.mjs +16 -507
- 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 +8 -3
- 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 +11 -11
- package/src/worker/index.ts +69 -524
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { normalizeLogLevel } from "./log.mjs";
|
|
2
|
+
|
|
3
|
+
const BOOLEAN_OPTIONS = new Set([
|
|
4
|
+
"help", "version", "quiet", "json", "verbose", "rotateSecrets", "forceWorker",
|
|
5
|
+
"daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials",
|
|
6
|
+
"printCredentials", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
|
|
7
|
+
"yes", "keepWorker", "allowInsecurePermissions", "showPaths",
|
|
8
|
+
]);
|
|
9
|
+
const VALUE_OPTIONS = new Set([
|
|
10
|
+
"workspace", "stateDir", "workerName", "profile", "execMode", "client", "logLevel", "logFormat",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const LOG_FORMATS = new Set(["text", "json"]);
|
|
14
|
+
|
|
15
|
+
const COMMAND_OPTIONS = {
|
|
16
|
+
start: new Set([
|
|
17
|
+
"workspace", "stateDir", "workerName", "quiet", "json", "verbose", "logLevel", "logFormat", "rotateSecrets", "forceWorker",
|
|
18
|
+
"daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials", "printCredentials",
|
|
19
|
+
"profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
|
|
20
|
+
]),
|
|
21
|
+
stdio: new Set(["workspace", "stateDir", "profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths", "verbose", "quiet", "logLevel", "logFormat"]),
|
|
22
|
+
"client-config": new Set(["workspace", "stateDir", "profile", "client", "json"]),
|
|
23
|
+
status: new Set(["workspace", "stateDir"]),
|
|
24
|
+
doctor: new Set(["workspace", "stateDir"]),
|
|
25
|
+
"full-test": new Set(["workspace", "stateDir", "json"]),
|
|
26
|
+
"rotate-secrets": new Set(["workspace", "stateDir", "workerName", "noPrintCredentials", "printMcpCredentials", "printCredentials", "quiet"]),
|
|
27
|
+
workspace: new Set(["workspace", "stateDir"]),
|
|
28
|
+
service: new Set(["workspace", "stateDir", "quiet"]),
|
|
29
|
+
autostart: new Set(["workspace", "stateDir", "quiet"]),
|
|
30
|
+
resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "showPaths", "json"]),
|
|
31
|
+
browser: new Set(["workspace", "stateDir", "json"]),
|
|
32
|
+
job: new Set(["workspace", "stateDir", "json", "yes"]),
|
|
33
|
+
uninstall: new Set(["stateDir", "keepWorker", "yes"]),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const SINGLE_WORKSPACE_POSITIONAL_COMMANDS = new Set(["start", "stdio", "status", "doctor", "full-test", "rotate-secrets"]);
|
|
37
|
+
const STATIC_POSITIONAL_RULES = Object.freeze({
|
|
38
|
+
"client-config": Object.freeze({ max: 1, tooMany: "client-config accepts at most one positional client name" }),
|
|
39
|
+
uninstall: Object.freeze({ max: 0, tooMany: "uninstall does not accept positional arguments" }),
|
|
40
|
+
});
|
|
41
|
+
const RESOURCE_POSITIONAL_LIMITS = Object.freeze({ add: 3, "generate-ssh-key": 3, remove: 2, check: 2 });
|
|
42
|
+
const JOB_POSITIONAL_LIMITS = Object.freeze({ read: 2, inspect: 2, cancel: 2, approve: 2, submit: 2 });
|
|
43
|
+
const ACTION_POSITIONAL_RULES = Object.freeze({
|
|
44
|
+
workspace(args) {
|
|
45
|
+
const action = String(args._[0] || "show");
|
|
46
|
+
return { max: action === "set" || action === "select" ? 2 : 1, tooMany: `workspace ${action} received too many positional arguments`, workspaceConflictAfter: 1 };
|
|
47
|
+
},
|
|
48
|
+
service(args) {
|
|
49
|
+
const action = String(args._[0] || "status");
|
|
50
|
+
return { max: ["install", "status", "stop", "uninstall", "remove"].includes(action) ? 2 : 1, tooMany: `service ${action} received too many positional arguments`, workspaceConflictAfter: 1 };
|
|
51
|
+
},
|
|
52
|
+
autostart(args) { return ACTION_POSITIONAL_RULES.service(args); },
|
|
53
|
+
resource(args) {
|
|
54
|
+
const action = String(args._[0] || "list");
|
|
55
|
+
return { max: RESOURCE_POSITIONAL_LIMITS[action] ?? 1, tooMany: `resource ${action} received too many positional arguments` };
|
|
56
|
+
},
|
|
57
|
+
browser(args) {
|
|
58
|
+
const action = String(args._[0] || "status");
|
|
59
|
+
return { max: 1, tooMany: `browser ${action} received too many positional arguments` };
|
|
60
|
+
},
|
|
61
|
+
job(args) {
|
|
62
|
+
const action = String(args._[0] || "list");
|
|
63
|
+
return { max: JOB_POSITIONAL_LIMITS[action] ?? 1, tooMany: `job ${action} received too many positional arguments` };
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export function normalizeCommand(argv) {
|
|
68
|
+
if (!argv.length || argv[0].startsWith("--")) return ["start", argv];
|
|
69
|
+
return [argv[0], argv.slice(1)];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseArgs(argv) {
|
|
73
|
+
const out = { _: [] };
|
|
74
|
+
let positionalOnly = false;
|
|
75
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
76
|
+
const raw = argv[index];
|
|
77
|
+
if (positionalOnly || raw === "-" || !raw.startsWith("--")) {
|
|
78
|
+
out._.push(raw);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (raw === "--") {
|
|
82
|
+
positionalOnly = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const separator = raw.indexOf("=");
|
|
86
|
+
const rawKey = raw.slice(2, separator >= 0 ? separator : undefined);
|
|
87
|
+
if (!rawKey) throw new Error("invalid empty option");
|
|
88
|
+
const key = toCamel(rawKey);
|
|
89
|
+
if (!BOOLEAN_OPTIONS.has(key) && !VALUE_OPTIONS.has(key)) throw new Error(`Unknown option: --${rawKey}`);
|
|
90
|
+
if (Object.prototype.hasOwnProperty.call(out, key)) throw new Error(`Duplicate option: --${rawKey}`);
|
|
91
|
+
if (BOOLEAN_OPTIONS.has(key)) {
|
|
92
|
+
out[key] = separator >= 0 ? parseBooleanOption(raw.slice(separator + 1), rawKey) : true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const value = separator >= 0 ? raw.slice(separator + 1) : argv[++index];
|
|
96
|
+
if (value === undefined || value === "" || value.startsWith("--")) throw new Error(`Option --${rawKey} requires a value`);
|
|
97
|
+
out[key] = value;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function validateCommandOptions(command, args) {
|
|
103
|
+
const allowed = COMMAND_OPTIONS[command];
|
|
104
|
+
if (!allowed) return;
|
|
105
|
+
for (const key of Object.keys(args)) {
|
|
106
|
+
if (key === "_" || key === "help" || key === "version") continue;
|
|
107
|
+
if (!allowed.has(key)) throw new Error(`Option --${toKebab(key)} is not valid for ${command}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function validateLoggingOptions(args = {}) {
|
|
112
|
+
if (args.quiet && args.verbose) throw new Error("--quiet and --verbose cannot be used together");
|
|
113
|
+
if (args.logLevel !== undefined && (args.quiet || args.verbose)) throw new Error("--log-level cannot be combined with --quiet or --verbose");
|
|
114
|
+
if (args.logLevel !== undefined) normalizeLogLevel(args.logLevel);
|
|
115
|
+
if (args.logFormat !== undefined && !LOG_FORMATS.has(String(args.logFormat).toLowerCase())) throw new Error("--log-format must be text or json");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function validatePositionals(command, args) {
|
|
119
|
+
const rule = positionalRule(command, args);
|
|
120
|
+
if (!rule) return;
|
|
121
|
+
if (args._.length > rule.max) throw new Error(rule.tooMany);
|
|
122
|
+
if (args.workspace && Number.isInteger(rule.workspaceConflictAfter) && args._.length > rule.workspaceConflictAfter) {
|
|
123
|
+
throw new Error("workspace path was provided both positionally and with --workspace");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function effectiveLogLevel(args = {}) {
|
|
128
|
+
if (args.logLevel !== undefined) return normalizeLogLevel(args.logLevel);
|
|
129
|
+
if (args.quiet) return "error";
|
|
130
|
+
if (args.verbose) return "debug";
|
|
131
|
+
return "info";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function effectiveLogFormat(args = {}) {
|
|
135
|
+
return String(args.logFormat || "text").toLowerCase();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function positionalRule(command, args) {
|
|
139
|
+
if (SINGLE_WORKSPACE_POSITIONAL_COMMANDS.has(command)) {
|
|
140
|
+
return { max: 1, tooMany: `${command} accepts at most one positional workspace path`, workspaceConflictAfter: 0 };
|
|
141
|
+
}
|
|
142
|
+
if (STATIC_POSITIONAL_RULES[command]) return STATIC_POSITIONAL_RULES[command];
|
|
143
|
+
return ACTION_POSITIONAL_RULES[command]?.(args) || null;
|
|
144
|
+
}
|
|
145
|
+
function parseBooleanOption(value, key) {
|
|
146
|
+
if (value === "true" || value === "1") return true;
|
|
147
|
+
if (value === "false" || value === "0") return false;
|
|
148
|
+
throw new Error(`Option --${key} expects true or false when using =`);
|
|
149
|
+
}
|
|
150
|
+
function toCamel(key) { return key.replace(/-([a-z])/g, (_, character) => character.toUpperCase()); }
|
|
151
|
+
function toKebab(value) { return String(value).replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`); }
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_POLICY_PROFILE,
|
|
3
|
+
DEFAULT_POLICY_REVISION,
|
|
4
|
+
POLICY_PROFILES,
|
|
5
|
+
normalizePolicy,
|
|
6
|
+
policyProfile,
|
|
7
|
+
} from "./policy.mjs";
|
|
8
|
+
|
|
9
|
+
const POLICY_OVERRIDE_KEYS = Object.freeze(["execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths"]);
|
|
10
|
+
|
|
11
|
+
export function resolvePolicy(args = {}, stored = {}) {
|
|
12
|
+
const hasStored = stored && typeof stored === "object" && (
|
|
13
|
+
typeof stored.allowWrite === "boolean" || typeof stored.allowExec === "boolean" || typeof stored.execMode === "string"
|
|
14
|
+
);
|
|
15
|
+
const explicitKeys = ["profile", ...POLICY_OVERRIDE_KEYS];
|
|
16
|
+
const hasExplicit = explicitKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key));
|
|
17
|
+
const base = { ...selectPolicyBase(args, stored, hasStored) };
|
|
18
|
+
if (!hasExplicit) return normalizePolicy(base);
|
|
19
|
+
applyPolicyOverrides(base, args);
|
|
20
|
+
if (args.profile === undefined || POLICY_OVERRIDE_KEYS.some((key) => Object.prototype.hasOwnProperty.call(args, key))) {
|
|
21
|
+
base.profile = "custom";
|
|
22
|
+
base.origin = "custom";
|
|
23
|
+
base.revision = DEFAULT_POLICY_REVISION;
|
|
24
|
+
}
|
|
25
|
+
return normalizePolicy(base);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function selectPolicyBase(args, stored, hasStored) {
|
|
29
|
+
if (args.profile !== undefined) {
|
|
30
|
+
const profile = String(args.profile).trim().toLowerCase();
|
|
31
|
+
if (!POLICY_PROFILES[profile]) throw new Error(`--profile must be one of: ${Object.keys(POLICY_PROFILES).join(", ")}`);
|
|
32
|
+
return policyProfile(profile, "explicit");
|
|
33
|
+
}
|
|
34
|
+
if (hasStored) return migrateLegacyPolicy(stored);
|
|
35
|
+
return policyProfile(DEFAULT_POLICY_PROFILE, "default");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function applyPolicyOverrides(policy, args) {
|
|
39
|
+
if (args.execMode !== undefined) {
|
|
40
|
+
const execMode = String(args.execMode).trim().toLowerCase();
|
|
41
|
+
if (!["off", "direct", "shell"].includes(execMode)) throw new Error("--exec-mode must be off, direct, or shell");
|
|
42
|
+
policy.execMode = execMode;
|
|
43
|
+
}
|
|
44
|
+
applyBooleanOverride(args, "noWrite", (enabled) => { policy.allowWrite = !enabled; });
|
|
45
|
+
applyBooleanOverride(args, "noExec", (enabled) => {
|
|
46
|
+
if (enabled) policy.execMode = "off";
|
|
47
|
+
else if (policy.execMode === "off") policy.execMode = "direct";
|
|
48
|
+
});
|
|
49
|
+
applyBooleanOverride(args, "fullEnv", (enabled) => { policy.minimalEnv = !enabled; });
|
|
50
|
+
applyBooleanOverride(args, "unrestrictedPaths", (enabled) => { policy.unrestrictedPaths = enabled; });
|
|
51
|
+
applyBooleanOverride(args, "absolutePaths", (enabled) => { policy.exposeAbsolutePaths = enabled; });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function applyBooleanOverride(args, key, apply) {
|
|
55
|
+
if (typeof args[key] === "boolean") apply(args[key]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function migrateLegacyPolicy(stored = {}) {
|
|
59
|
+
if (stored.origin === "default" && Number(stored.revision || 0) < DEFAULT_POLICY_REVISION) {
|
|
60
|
+
return policyProfile(DEFAULT_POLICY_PROFILE, "default");
|
|
61
|
+
}
|
|
62
|
+
if (stored.origin === "migrated" && Number(stored.revision || 0) < DEFAULT_POLICY_REVISION) {
|
|
63
|
+
return policyProfile(DEFAULT_POLICY_PROFILE, "migrated");
|
|
64
|
+
}
|
|
65
|
+
if (stored.origin) return normalizePolicy(stored);
|
|
66
|
+
const normalized = normalizePolicy(stored);
|
|
67
|
+
const looksLikeLegacyImplicitDefault = (
|
|
68
|
+
normalized.profile === "custom"
|
|
69
|
+
&& normalized.allowWrite === true
|
|
70
|
+
&& normalized.execMode === "shell"
|
|
71
|
+
&& normalized.unrestrictedPaths === false
|
|
72
|
+
&& normalized.minimalEnv === true
|
|
73
|
+
&& normalized.exposeAbsolutePaths === false
|
|
74
|
+
);
|
|
75
|
+
if (looksLikeLegacyImplicitDefault) return policyProfile("full", "migrated");
|
|
76
|
+
return normalizePolicy({ ...normalized, origin: "legacy-preserved", revision: DEFAULT_POLICY_REVISION });
|
|
77
|
+
}
|