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
package/src/local/service.mjs
CHANGED
|
@@ -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(
|
|
469
|
+
return [spec.node, ...daemonArgs(spec)].map(windowsCommandLineArgument).join(" ");
|
|
469
470
|
}
|
|
470
471
|
|
|
471
|
-
function
|
|
472
|
-
|
|
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) {
|
package/src/local/stdio.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/local/tools.mjs
CHANGED
|
@@ -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;
|