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
@@ -226,6 +226,7 @@ export function daemonArgs(spec) {
226
226
  "--state-dir", spec.stateRoot,
227
227
  "--no-print-credentials",
228
228
  "--log-level", "warn",
229
+ "--log-format", "json",
229
230
  ];
230
231
  }
231
232
 
@@ -465,11 +466,15 @@ async function statusWindowsTask() {
465
466
  }
466
467
 
467
468
  function windowsCommand(spec) {
468
- return [spec.node, ...daemonArgs(spec)].map(winQuote).join(" ");
469
+ return [spec.node, ...daemonArgs(spec)].map(windowsCommandLineArgument).join(" ");
469
470
  }
470
471
 
471
- function winQuote(value) {
472
- return `"${String(value).replaceAll('"', '\\"')}"`;
472
+ export function windowsCommandLineArgument(value) {
473
+ const text = String(value);
474
+ if (text.includes("\0")) throw new Error("Windows command-line argument contains a NUL byte");
475
+ const escapedQuotes = text.replace(/(\\*)"/g, (_match, slashes) => `${slashes}${slashes}\\"`);
476
+ const escapedTrailingSlashes = escapedQuotes.replace(/(\\+)$/, (slashes) => `${slashes}${slashes}`);
477
+ return `"${escapedTrailingSlashes}"`;
473
478
  }
474
479
 
475
480
  export function systemdQuote(value) {
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { LocalRuntime } from "./runtime.mjs";
4
4
  import { classifyOperationalError, createLogger } from "./log.mjs";
5
+ import { publicError } from "./errors.mjs";
5
6
  import {
6
7
  MCP_INSTRUCTIONS,
7
8
  MCP_PROTOCOL_VERSION,
@@ -14,7 +15,6 @@ import {
14
15
  } from "./tools.mjs";
15
16
 
16
17
  const MAX_LINE_BYTES = 8 * 1024 * 1024;
17
- const SLOW_TOOL_CALL_MS = 30_000;
18
18
  const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version);
19
19
 
20
20
  export async function runStdioServer({ workspace, policy, logLevel = "info", jobRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "" }) {
@@ -115,21 +115,13 @@ export async function runStdioServer({ workspace, policy, logLevel = "info", job
115
115
  return;
116
116
  }
117
117
  if (key) pending.set(key, callId);
118
- const started = Date.now();
119
- logger.debug("tool call started", { call_id: callId.slice(0, 20), tool: name });
120
118
  try {
121
- const result = await runtime.executeTool(name, args, { callId });
119
+ const result = await runtime.executeTool(name, args, { callId, origin: "stdio" });
122
120
  send(rpcResult(message.id, toolResult(result)));
123
- const durationMs = Date.now() - started;
124
- logger.debug(durationMs >= SLOW_TOOL_CALL_MS ? "slow tool call completed" : "tool call completed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs });
125
121
  } catch (error) {
126
- const safeError = runtime.safeErrorMessage(error, args);
127
- send(rpcResult(message.id, toolResult({ error: safeError }, true)));
128
- const durationMs = Date.now() - started;
129
- logger.debug("tool call failed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs, error_class: classifyOperationalError(error) });
122
+ send(rpcResult(message.id, toolResult({ error: publicError(error) }, true)));
130
123
  } finally {
131
124
  if (key) pending.delete(key);
132
- runtime.finishCall(callId);
133
125
  }
134
126
  }
135
127
 
@@ -0,0 +1,102 @@
1
+ import { errorCode, normalizeBridgeError } from "./errors.mjs";
2
+
3
+ export class ToolExecutor {
4
+ constructor(options = {}) {
5
+ this.handlers = options.handlers || {};
6
+ this.policyGate = options.policyGate;
7
+ this.callRegistry = options.callRegistry;
8
+ this.observability = options.observability;
9
+ this.logger = options.logger || console;
10
+ this.safeMessage = typeof options.safeMessage === "function" ? options.safeMessage : (error) => String(error?.message || error || "operation failed");
11
+ this.slowMs = Number.isFinite(Number(options.slowMs)) ? Math.max(1, Number(options.slowMs)) : 30_000;
12
+ this.pipeline = composeMiddleware([
13
+ lifecycleMiddleware(this.callRegistry),
14
+ observabilityMiddleware(this.observability, this.logger, this.safeMessage, this.slowMs),
15
+ authorizeMiddleware(this.policyGate),
16
+ ], invokeHandler(this.handlers));
17
+ }
18
+
19
+ execute(tool, args = {}, request = {}) {
20
+ return this.pipeline({ tool: String(tool || ""), args: isRecord(args) ? args : {}, request });
21
+ }
22
+ }
23
+
24
+ export function composeMiddleware(middleware, terminal) {
25
+ return middleware.reduceRight((next, current) => (operation) => current(operation, next), terminal);
26
+ }
27
+
28
+ function authorizeMiddleware(policyGate) {
29
+ return async (operation, next) => {
30
+ policyGate.assert(operation.tool);
31
+ return next(operation);
32
+ };
33
+ }
34
+
35
+ function lifecycleMiddleware(callRegistry) {
36
+ return async (operation, next) => {
37
+ const context = callRegistry.open({
38
+ callId: operation.request.callId,
39
+ tool: operation.tool,
40
+ origin: operation.request.origin || "local",
41
+ timeoutMs: operation.request.timeoutMs,
42
+ });
43
+ operation.context = { ...operation.request.context, ...context };
44
+ try {
45
+ callRegistry.throwIfCancelled(operation.context);
46
+ const result = await next(operation);
47
+ callRegistry.throwIfCancelled(operation.context);
48
+ return result;
49
+ } finally {
50
+ callRegistry.finish(context.callId);
51
+ }
52
+ };
53
+ }
54
+
55
+ function observabilityMiddleware(observability, logger, safeMessage, slowMs) {
56
+ return async (operation, next) => {
57
+ const started = Date.now();
58
+ observability.start(operation.tool);
59
+ logger.event?.("debug", "tool.call.started", {
60
+ call_id: shortCallId(operation.context.callId),
61
+ tool: operation.tool,
62
+ origin: operation.context.origin,
63
+ });
64
+ try {
65
+ const result = await next(operation);
66
+ const durationMs = Date.now() - started;
67
+ const slow = durationMs >= slowMs;
68
+ observability.finish(operation.tool, { status: "completed", durationMs, slow });
69
+ logger.event?.("debug", slow ? "tool.call.slow" : "tool.call.completed", {
70
+ call_id: shortCallId(operation.context.callId), tool: operation.tool, origin: operation.context.origin, duration_ms: durationMs,
71
+ });
72
+ return result;
73
+ } catch (error) {
74
+ const normalized = normalizeBridgeError(error, { safeMessage: () => safeMessage(error, operation.args) });
75
+ const durationMs = Date.now() - started;
76
+ const code = errorCode(normalized);
77
+ const status = code === "cancelled" ? "cancelled" : code === "timeout" ? "timeout" : "failed";
78
+ observability.finish(operation.tool, { status, durationMs, errorCode: code, slow: durationMs >= slowMs });
79
+ logger.event?.("debug", "tool.call.failed", {
80
+ call_id: shortCallId(operation.context.callId), tool: operation.tool, origin: operation.context.origin,
81
+ duration_ms: durationMs, error_code: code, retryable: normalized.retryable,
82
+ });
83
+ throw normalized;
84
+ }
85
+ };
86
+ }
87
+
88
+ function invokeHandler(handlers) {
89
+ return async (operation) => {
90
+ const handler = handlers[operation.tool];
91
+ if (typeof handler !== "function") throw new Error(`runtime handler is missing for tool: ${operation.tool}`);
92
+ return handler(operation.args, operation.context);
93
+ };
94
+ }
95
+
96
+ function shortCallId(value) {
97
+ return String(value || "").slice(0, 20);
98
+ }
99
+
100
+ function isRecord(value) {
101
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
102
+ }
@@ -1,110 +1,36 @@
1
- import catalog from "../shared/tool-catalog.json" with { type: "json" };
2
1
  import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
2
+ export {
3
+ DEFAULT_POLICY_PROFILE,
4
+ DEFAULT_POLICY_REVISION,
5
+ POLICY_AVAILABILITY,
6
+ POLICY_ORIGINS,
7
+ POLICY_PROFILES,
8
+ PolicyGate,
9
+ allToolNames,
10
+ assertCanonicalFullPolicy,
11
+ assertToolAllowed,
12
+ isCanonicalFullPolicy,
13
+ normalizePolicy,
14
+ policyAllowsAvailability,
15
+ policyAllowsTool,
16
+ policyCapabilitiesEqual,
17
+ policyProfile,
18
+ toolDefinition,
19
+ toolNamesForPolicy,
20
+ toolsForPolicy,
21
+ } from "./policy.mjs";
3
22
 
4
23
  export const SERVER_NAME = String(serverMetadata.name);
5
24
  export const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
6
25
  export const MCP_SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(serverMetadata.supportedProtocolVersions.map((value) => String(value)));
7
-
8
26
  export const MCP_INSTRUCTIONS = Object.freeze(serverMetadata.instructions.map((value) => String(value))).join("\n");
9
27
 
10
-
11
- export const DEFAULT_POLICY_PROFILE = "full";
12
- export const DEFAULT_POLICY_REVISION = 3;
13
-
14
- export const POLICY_PROFILES = Object.freeze({
15
- review: Object.freeze({ profile: "review", allowWrite: false, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
16
- edit: Object.freeze({ profile: "edit", allowWrite: true, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
17
- agent: Object.freeze({ profile: "agent", allowWrite: true, execMode: "direct", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
18
- full: Object.freeze({ profile: "full", allowWrite: true, execMode: "shell", unrestrictedPaths: true, minimalEnv: false, exposeAbsolutePaths: true }),
19
- });
20
-
21
- const POLICY_ORIGINS = new Set(["default", "explicit", "custom", "migrated", "legacy-preserved"]);
22
-
23
- export function policyProfile(name, origin = "explicit") {
24
- const profile = String(name || "").trim().toLowerCase();
25
- if (!POLICY_PROFILES[profile]) throw new Error(`unknown policy profile: ${profile}`);
26
- return normalizePolicy({ ...POLICY_PROFILES[profile], origin, revision: DEFAULT_POLICY_REVISION });
27
- }
28
-
29
- export function normalizePolicy(policy = {}) {
30
- const execMode = ["off", "direct", "shell"].includes(policy.execMode)
31
- ? policy.execMode
32
- : policy.allowExec === true
33
- ? "shell"
34
- : "off";
35
- const origin = POLICY_ORIGINS.has(policy.origin) ? policy.origin : "custom";
36
- const revision = Number.isInteger(policy.revision) && policy.revision > 0 ? policy.revision : DEFAULT_POLICY_REVISION;
37
- const requestedProfile = typeof policy.profile === "string" && policy.profile ? policy.profile : "custom";
38
- const normalized = {
39
- profile: requestedProfile,
40
- origin,
41
- revision,
42
- allowWrite: policy.allowWrite === true,
43
- allowExec: execMode !== "off",
44
- execMode,
45
- unrestrictedPaths: policy.unrestrictedPaths === true,
46
- minimalEnv: policy.minimalEnv !== false,
47
- exposeAbsolutePaths: policy.exposeAbsolutePaths === true,
48
- };
49
- if (POLICY_PROFILES[requestedProfile]) {
50
- const canonical = POLICY_PROFILES[requestedProfile];
51
- normalized.allowWrite = canonical.allowWrite;
52
- normalized.allowExec = canonical.execMode !== "off";
53
- normalized.execMode = canonical.execMode;
54
- normalized.unrestrictedPaths = canonical.unrestrictedPaths;
55
- normalized.minimalEnv = canonical.minimalEnv;
56
- normalized.exposeAbsolutePaths = canonical.exposeAbsolutePaths;
57
- }
58
- return normalized;
59
- }
60
-
61
- export function isCanonicalFullPolicy(policy = {}) {
62
- return policyCapabilitiesEqual(normalizePolicy(policy), POLICY_PROFILES.full);
63
- }
64
-
65
- export function assertCanonicalFullPolicy(policy = {}) {
66
- const normalized = normalizePolicy(policy);
67
- if (!isCanonicalFullPolicy(normalized) || normalized.profile !== "full") {
68
- throw new Error("full profile invariant failed: full must enable writes, shell execution, unrestricted paths, full parent environment, and absolute paths");
69
- }
70
- const exposed = toolsForPolicy(normalized);
71
- if (exposed.length !== catalog.length) throw new Error("full profile invariant failed: complete tool catalog is not exposed");
72
- return normalized;
73
- }
74
-
75
- function policyCapabilitiesEqual(left, right) {
76
- return left.allowWrite === right.allowWrite
77
- && left.execMode === right.execMode
78
- && left.unrestrictedPaths === right.unrestrictedPaths
79
- && left.minimalEnv === right.minimalEnv
80
- && left.exposeAbsolutePaths === right.exposeAbsolutePaths;
81
- }
82
-
83
- export function toolsForPolicy(policy = {}) {
84
- const normalized = normalizePolicy(policy);
85
- return catalog
86
- .filter((tool) => isAvailable(tool.availability, normalized))
87
- .map(({ availability, ...tool }) => structuredClone(tool));
88
- }
89
-
90
- export function toolNamesForPolicy(policy = {}) {
91
- return toolsForPolicy(policy).map((tool) => tool.name);
92
- }
93
-
94
- export function allToolNames() {
95
- return catalog.map((tool) => tool.name);
96
- }
97
-
98
-
99
28
  export function toolResult(value, isError = false) {
100
29
  const special = specialMcpResult(value);
101
30
  if (special) return { ...special, isError };
102
31
  const structuredContent = toStructuredContent(value);
103
32
  const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
104
- const result = {
105
- content: [{ type: "text", text }],
106
- isError,
107
- };
33
+ const result = { content: [{ type: "text", text }], isError };
108
34
  if (structuredContent) result.structuredContent = structuredContent;
109
35
  return result;
110
36
  }
@@ -120,15 +46,6 @@ export function rpcError(id, code, message, data) {
120
46
  return { jsonrpc: "2.0", id: id ?? null, error };
121
47
  }
122
48
 
123
- function isAvailable(availability, policy) {
124
- if (availability === "always") return true;
125
- if (availability === "write") return policy.allowWrite;
126
- if (availability === "direct-exec") return policy.execMode === "direct" || policy.execMode === "shell";
127
- if (availability === "shell-exec") return policy.execMode === "shell";
128
- if (availability === "full") return policy.profile === "full" && isCanonicalFullPolicy(policy);
129
- return false;
130
- }
131
-
132
49
  function specialMcpResult(value) {
133
50
  const special = value && typeof value === "object" && !Array.isArray(value) ? value.$mcp : null;
134
51
  if (!special || typeof special !== "object" || !Array.isArray(special.content)) return null;