machine-bridge-mcp 1.2.7 → 1.2.9
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 +23 -0
- package/CONTRIBUTING.md +37 -19
- package/README.md +120 -410
- package/SECURITY.md +2 -0
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +13 -4
- package/docs/AUDIT.md +22 -0
- package/docs/ENGINEERING.md +2 -2
- package/docs/OVERVIEW.md +113 -0
- package/docs/PROJECT_STANDARDS.md +9 -5
- package/docs/RELEASING.md +78 -33
- package/docs/TESTING.md +14 -7
- package/docs/THREAT_MODEL.md +142 -0
- package/package.json +18 -6
- package/scripts/check-plan.mjs +91 -0
- package/scripts/coverage-check.mjs +22 -0
- package/scripts/github-push.mjs +49 -0
- package/scripts/github-release.mjs +15 -8
- package/scripts/local-release-acceptance.mjs +186 -0
- package/scripts/release-acceptance.mjs +181 -0
- package/scripts/release-impact-check.mjs +19 -3
- package/scripts/release-state.mjs +1 -1
- package/scripts/run-checks.mjs +29 -0
- package/scripts/start-release-candidate.mjs +113 -0
- package/src/local/agent-context-projection.mjs +158 -0
- package/src/local/agent-context.mjs +23 -332
- package/src/local/agent-skill-discovery.mjs +230 -0
- package/src/local/agent-text-file.mjs +41 -0
- package/src/local/browser-bridge-http.mjs +48 -0
- package/src/local/browser-bridge.mjs +48 -222
- package/src/local/browser-broker-routes.mjs +136 -0
- package/src/local/browser-broker-server.mjs +59 -0
- package/src/local/browser-request-registry.mjs +67 -0
- package/src/local/managed-job-lock.mjs +99 -0
- package/src/local/managed-job-projection.mjs +68 -0
- package/src/local/managed-job-runner.mjs +73 -0
- package/src/local/managed-job-storage.mjs +93 -0
- package/src/local/managed-jobs.mjs +12 -297
- package/src/local/runtime-paths.mjs +107 -0
- package/src/local/runtime-relay.mjs +73 -0
- package/src/local/runtime-tool-handlers.mjs +66 -0
- package/src/local/runtime.mjs +22 -204
- package/src/local/windows-launcher.mjs +57 -0
- package/src/local/windows-service.mjs +7 -56
- package/src/worker/index.ts +9 -118
- package/src/worker/mcp-jsonrpc.ts +94 -0
- package/src/worker/oauth-authorization-page.ts +70 -0
- package/src/worker/oauth-controller.ts +9 -58
- package/src/worker/websocket-protocol.ts +24 -0
- package/tsconfig.local.json +6 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { mkdirSync, mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} RuntimeRedactionOptions
|
|
8
|
+
* @property {unknown} error
|
|
9
|
+
* @property {unknown} toolArgs
|
|
10
|
+
* @property {string} workspace
|
|
11
|
+
* @property {string} workspaceInput
|
|
12
|
+
* @property {string} runtimeDir
|
|
13
|
+
* @property {string | undefined} home
|
|
14
|
+
* @property {(value: string) => string} displayPath
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @param {string} statePath */
|
|
18
|
+
export function stateRootFromProfileStatePath(statePath) {
|
|
19
|
+
const absolute = resolve(statePath);
|
|
20
|
+
if (basename(absolute) !== "state.json") throw new Error("local resource state path is invalid");
|
|
21
|
+
const profileDir = dirname(absolute);
|
|
22
|
+
const profilesDir = dirname(profileDir);
|
|
23
|
+
if (basename(profilesDir) !== "profiles") throw new Error("local resource state path is outside the expected profile layout");
|
|
24
|
+
return dirname(profilesDir);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @param {string} root @param {string} target */
|
|
28
|
+
export function assertContainedPath(root, target) {
|
|
29
|
+
const rel = relative(root, target);
|
|
30
|
+
if (rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel))) return;
|
|
31
|
+
throw new Error("path is outside the configured workspace; restart with --unrestricted-paths to allow it");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createRuntimeDir() {
|
|
35
|
+
const root = mkdtempSync(join(tmpdir(), "machine-bridge-mcp-"));
|
|
36
|
+
for (const name of ["home", "tmp", "cache"]) mkdirSync(join(root, name), { recursive: true, mode: 0o700 });
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {unknown} message @param {RuntimeRedactionOptions} options */
|
|
41
|
+
export function redactRuntimeErrorMessage(message, {
|
|
42
|
+
error,
|
|
43
|
+
toolArgs,
|
|
44
|
+
workspace,
|
|
45
|
+
workspaceInput,
|
|
46
|
+
runtimeDir,
|
|
47
|
+
home,
|
|
48
|
+
displayPath,
|
|
49
|
+
}) {
|
|
50
|
+
let redacted = String(message);
|
|
51
|
+
for (const prefix of equivalentPathPrefixes(workspace, workspaceInput)) redacted = replacePathPrefix(redacted, prefix, ".");
|
|
52
|
+
for (const prefix of equivalentPathPrefixes(runtimeDir)) redacted = replacePathPrefix(redacted, prefix, "<runtime>");
|
|
53
|
+
if (home) redacted = replacePathPrefix(redacted, resolve(home), "<home>");
|
|
54
|
+
for (const candidate of collectToolPathCandidates(error, toolArgs, workspaceInput)) {
|
|
55
|
+
const absolute = isAbsolute(candidate) ? resolve(candidate) : resolve(workspaceInput, candidate);
|
|
56
|
+
const replacement = displayPath(absolute);
|
|
57
|
+
for (const prefix of equivalentPathPrefixes(candidate, absolute)) redacted = replacePathPrefix(redacted, prefix, replacement);
|
|
58
|
+
}
|
|
59
|
+
return redacted;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @param {unknown} error @param {unknown} toolArgs @param {string} workspace */
|
|
63
|
+
function collectToolPathCandidates(error, toolArgs, workspace) {
|
|
64
|
+
/** @type {Set<string>} */
|
|
65
|
+
const candidates = new Set();
|
|
66
|
+
const pathError = error && typeof error === "object"
|
|
67
|
+
? /** @type {{ path?: unknown, dest?: unknown }} */ (error)
|
|
68
|
+
: {};
|
|
69
|
+
for (const value of [pathError.path, pathError.dest]) {
|
|
70
|
+
if (typeof value === "string" && value) candidates.add(value);
|
|
71
|
+
}
|
|
72
|
+
/** @param {unknown} value @param {string} [key] @param {number} [depth] */
|
|
73
|
+
const visit = (value, key = "", depth = 0) => {
|
|
74
|
+
if (depth > 5 || value === null || value === undefined) return;
|
|
75
|
+
if (typeof value === "string") {
|
|
76
|
+
if (/(?:^|[_-])(?:path|cwd|workspace|root|directory|dir)(?:$|[_-])/i.test(key) && value && !value.includes("\0")) candidates.add(value);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
for (const item of value.slice(0, 64)) visit(item, key, depth + 1);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (typeof value !== "object") return;
|
|
84
|
+
for (const [childKey, child] of Object.entries(/** @type {Record<string, unknown>} */ (value)).slice(0, 128)) {
|
|
85
|
+
visit(child, childKey, depth + 1);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
visit(toolArgs);
|
|
89
|
+
candidates.delete(workspace);
|
|
90
|
+
return [...candidates].sort((left, right) => right.length - left.length);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @param {...(string | null | undefined)} values */
|
|
94
|
+
function equivalentPathPrefixes(...values) {
|
|
95
|
+
const prefixes = new Set(values.filter(Boolean).map((value) => String(value)));
|
|
96
|
+
for (const value of [...prefixes]) {
|
|
97
|
+
if (value.startsWith("/private/")) prefixes.add(value.slice("/private".length));
|
|
98
|
+
else if (value.startsWith("/") && ["/var/", "/tmp/", "/etc/"].some((prefix) => value.startsWith(prefix))) prefixes.add(`/private${value}`);
|
|
99
|
+
}
|
|
100
|
+
return [...prefixes].sort((left, right) => right.length - left.length);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @param {string} message @param {string} pathValue @param {string} replacement */
|
|
104
|
+
function replacePathPrefix(message, pathValue, replacement) {
|
|
105
|
+
if (!pathValue) return message;
|
|
106
|
+
return message.split(pathValue).join(replacement);
|
|
107
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { RelayConnection } from "./relay-connection.mjs";
|
|
3
|
+
import { MCP_SUPPORTED_PROTOCOL_VERSIONS, SERVER_NAME } from "./tools.mjs";
|
|
4
|
+
import { normalizeAccountRole } from "./account-access.mjs";
|
|
5
|
+
import { clampInteger } from "./numbers.mjs";
|
|
6
|
+
import { isPlainRecord } from "./records.mjs";
|
|
7
|
+
|
|
8
|
+
export const MAX_RELAY_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
export function createRuntimeRelayConnection(runtime, { workerUrl, secret, expectedVersion, onFatal }) {
|
|
11
|
+
if (!workerUrl) return null;
|
|
12
|
+
return new RelayConnection({
|
|
13
|
+
workerUrl,
|
|
14
|
+
secret,
|
|
15
|
+
logger: runtime.logger,
|
|
16
|
+
maxPayload: MAX_RELAY_MESSAGE_BYTES,
|
|
17
|
+
expectedServer: SERVER_NAME,
|
|
18
|
+
expectedVersion: String(expectedVersion || ""),
|
|
19
|
+
helloMessage: () => ({
|
|
20
|
+
type: "hello",
|
|
21
|
+
tools: runtime.tools(),
|
|
22
|
+
policy: runtime.policy,
|
|
23
|
+
protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
|
|
24
|
+
}),
|
|
25
|
+
onMessage: (data, relayContext) => handleRelayData(runtime, data, relayContext),
|
|
26
|
+
onDisconnect: () => runtime.handleRelayDisconnect(),
|
|
27
|
+
onSuperseded: () => {
|
|
28
|
+
runtime.terminateActiveProcesses("SIGKILL");
|
|
29
|
+
runtime.processSessionManager.clear();
|
|
30
|
+
runtime.onSuperseded?.();
|
|
31
|
+
},
|
|
32
|
+
onFatal: (error) => {
|
|
33
|
+
runtime.terminateActiveProcesses("SIGKILL");
|
|
34
|
+
runtime.processSessionManager.clear();
|
|
35
|
+
onFatal?.(error);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function normalizeRelayToolCall(message) {
|
|
41
|
+
const id = typeof message.id === "string" && message.id.length <= 256 ? message.id : "";
|
|
42
|
+
const tool = typeof message.tool === "string" && message.tool.length <= 128 ? message.tool : "";
|
|
43
|
+
const argumentsValue = message.arguments === undefined ? {} : message.arguments;
|
|
44
|
+
const authorization = normalizeRelayAuthorization(message.authorization);
|
|
45
|
+
if (!id || !tool || !isPlainRecord(argumentsValue) || !authorization) return { ok: false, id };
|
|
46
|
+
return {
|
|
47
|
+
ok: true,
|
|
48
|
+
id,
|
|
49
|
+
tool,
|
|
50
|
+
arguments: argumentsValue,
|
|
51
|
+
authorization,
|
|
52
|
+
timeoutMs: clampInteger(message.timeout_ms, 60_000, 1000, 610_000),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleRelayData(runtime, data, relayContext = {}) {
|
|
57
|
+
const raw = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
|
|
58
|
+
if (Buffer.byteLength(raw) > MAX_RELAY_MESSAGE_BYTES) {
|
|
59
|
+
runtime.handleRelayProtocolViolation("server_message_too_large");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
return runtime.handleMessage(raw, relayContext);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeRelayAuthorization(value) {
|
|
66
|
+
if (!isPlainRecord(value)) return null;
|
|
67
|
+
const accountId = typeof value.account_id === "string" && /^acct_[A-Za-z0-9_-]{20,96}$/.test(value.account_id) ? value.account_id : "";
|
|
68
|
+
const accountVersion = Number(value.account_version);
|
|
69
|
+
let role;
|
|
70
|
+
try { role = normalizeAccountRole(value.role); } catch { return null; }
|
|
71
|
+
if (!accountId || !Number.isInteger(accountVersion) || accountVersion < 1) return null;
|
|
72
|
+
return Object.freeze({ account_id: accountId, account_version: accountVersion, role });
|
|
73
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { clampInteger } from "./numbers.mjs";
|
|
2
|
+
|
|
3
|
+
const RUNTIME_TOOL_HANDLERS = Object.freeze({
|
|
4
|
+
server_info: (runtime) => runtime.runtimeInfo(),
|
|
5
|
+
project_overview: (runtime, _args, context) => runtime.projectOverview(context),
|
|
6
|
+
session_bootstrap: (runtime, args, context) => runtime.sessionBootstrap(args, context),
|
|
7
|
+
agent_context: (runtime, args, context) => runtime.agentContextManager.agentContext(args, context),
|
|
8
|
+
resolve_task_capabilities: (runtime, args, context) => runtime.resolveTaskCapabilities(args, context),
|
|
9
|
+
list_local_skills: (runtime, args, context) => runtime.agentContextManager.listLocalSkills(args, context),
|
|
10
|
+
load_local_skill: (runtime, args, context) => runtime.agentContextManager.loadLocalSkill(args, context),
|
|
11
|
+
list_local_commands: (runtime, args, context) => runtime.agentContextManager.listLocalCommands(args, context),
|
|
12
|
+
run_local_command: (runtime, args, context) => runtime.runLocalCommand(args, context),
|
|
13
|
+
list_local_applications: (runtime, args, context) => runtime.appAutomationManager.listApplications(args, context),
|
|
14
|
+
open_local_application: (runtime, args, context) => runtime.appAutomationManager.openApplication(args, context),
|
|
15
|
+
inspect_local_application: (runtime, args, context) => runtime.appAutomationManager.inspectApplication(args, context),
|
|
16
|
+
operate_local_application: (runtime, args, context) => runtime.appAutomationManager.operateApplication(args, context),
|
|
17
|
+
browser_status: (runtime, _args, context) => runtime.browserBridgeManager.status(context),
|
|
18
|
+
pair_browser_extension: (runtime, args, context) => runtime.browserBridgeManager.pair(args, context),
|
|
19
|
+
browser_list_tabs: (runtime, args, context) => runtime.browserBridgeManager.listTabs(args, context),
|
|
20
|
+
browser_manage_tabs: (runtime, args, context) => runtime.browserBridgeManager.manageTabs(args, context),
|
|
21
|
+
browser_wait: (runtime, args, context) => runtime.browserBridgeManager.wait(args, context),
|
|
22
|
+
browser_get_source: (runtime, args, context) => runtime.browserBridgeManager.getSource(args, context),
|
|
23
|
+
browser_inspect_page: (runtime, args, context) => runtime.browserBridgeManager.inspectPage(args, context),
|
|
24
|
+
browser_action: (runtime, args, context) => runtime.browserBridgeManager.act(args, context),
|
|
25
|
+
browser_fill_form: (runtime, args, context) => runtime.browserBridgeManager.fillForm(args, context),
|
|
26
|
+
browser_screenshot: (runtime, args, context) => runtime.browserBridgeManager.screenshot(args, context),
|
|
27
|
+
browser_upload_files: (runtime, args, context) => runtime.browserBridgeManager.uploadFiles(args, context),
|
|
28
|
+
list_roots: (runtime) => runtime.listRoots(),
|
|
29
|
+
list_dir: (runtime, args, context) => runtime.listDir(args.path || ".", context),
|
|
30
|
+
list_files: (runtime, args, context) => runtime.listFiles(args.path || ".", clampInteger(args.max_files, 1000, 1, 10000), context),
|
|
31
|
+
read_file: (runtime, args, context) => runtime.readFile(args, context),
|
|
32
|
+
view_image: (runtime, args, context) => runtime.viewImage(args, context),
|
|
33
|
+
write_file: (runtime, args, context) => runtime.writeFile(args, context),
|
|
34
|
+
edit_file: (runtime, args, context) => runtime.editFile(args, context),
|
|
35
|
+
apply_patch: (runtime, args, context) => runtime.applyPatch(args, context),
|
|
36
|
+
search_text: (runtime, args, context) => runtime.searchText(args, context),
|
|
37
|
+
git_status: (runtime, args, context) => runtime.gitStatus(args, context),
|
|
38
|
+
git_diff: (runtime, args, context) => runtime.gitDiff(args, context),
|
|
39
|
+
git_log: (runtime, args, context) => runtime.gitLog(args, context),
|
|
40
|
+
git_show: (runtime, args, context) => runtime.gitShow(args, context),
|
|
41
|
+
diagnose_runtime: (runtime, _args, context) => runtime.diagnoseRuntime(context),
|
|
42
|
+
list_local_resources: (runtime) => runtime.managedJobManager.listResources(),
|
|
43
|
+
generate_ssh_key_resource: (runtime, args, context) => runtime.generateSshKeyResource(args, context),
|
|
44
|
+
stage_job: (runtime, args) => runtime.managedJobManager.stage(args),
|
|
45
|
+
start_job: (runtime, args) => runtime.managedJobManager.start(args),
|
|
46
|
+
list_jobs: (runtime, args) => runtime.managedJobManager.list(args),
|
|
47
|
+
read_job: (runtime, args) => runtime.managedJobManager.read(args),
|
|
48
|
+
cancel_job: (runtime, args) => runtime.managedJobManager.cancel(args),
|
|
49
|
+
run_process: (runtime, args, context) => runtime.runDirectProcess(args, context),
|
|
50
|
+
start_process: (runtime, args, context) => runtime.processSessionManager.start(args, context),
|
|
51
|
+
read_process: (runtime, args, context) => runtime.processSessionManager.read(args, context),
|
|
52
|
+
write_process: (runtime, args, context) => runtime.processSessionManager.write(args, context),
|
|
53
|
+
kill_process: (runtime, args, context) => runtime.processSessionManager.kill(args, context),
|
|
54
|
+
exec_command: (runtime, args, context) => runtime.execCommand(args.command, clampInteger(args.timeout_seconds, 120, 1, 600), context),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export function runtimeToolHandlerNames() {
|
|
58
|
+
return Object.keys(RUNTIME_TOOL_HANDLERS);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function bindRuntimeToolHandlers(runtime) {
|
|
62
|
+
return Object.fromEntries(Object.entries(RUNTIME_TOOL_HANDLERS).map(([name, handler]) => [
|
|
63
|
+
name,
|
|
64
|
+
(args, context) => handler(runtime, args, context),
|
|
65
|
+
]));
|
|
66
|
+
}
|
package/src/local/runtime.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { realpathSync, rmSync } from "node:fs";
|
|
2
2
|
import { lstat, realpath, stat } from "node:fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { isRelayReadyContext, RelayConnection } from "./relay-connection.mjs";
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { isRelayReadyContext } from "./relay-connection.mjs";
|
|
6
5
|
import { ProcessSessionManager } from "./process-sessions.mjs";
|
|
7
6
|
import { MAX_CONCURRENT_TOOL_CALLS } from "./execution-limits.mjs";
|
|
8
7
|
export { MAX_COMMAND_BYTES } from "./process-contract.mjs";
|
|
9
|
-
import {
|
|
8
|
+
import { normalizePolicy, PolicyGate } from "./tools.mjs";
|
|
10
9
|
import { publicError } from "./errors.mjs";
|
|
11
10
|
import { ProcessTracker } from "./process-tracker.mjs";
|
|
12
11
|
import { CallRegistry } from "./call-registry.mjs";
|
|
@@ -26,75 +25,22 @@ import { AppAutomationManager } from "./app-automation.mjs";
|
|
|
26
25
|
import { BrowserBridgeManager } from "./browser-bridge.mjs";
|
|
27
26
|
import { CapabilityObserver } from "./capability-observer.mjs";
|
|
28
27
|
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
29
|
-
import { clampInteger } from "./numbers.mjs";
|
|
30
28
|
import { isPlainRecord } from "./records.mjs";
|
|
31
|
-
import { AccountAccessGate
|
|
29
|
+
import { AccountAccessGate } from "./account-access.mjs";
|
|
32
30
|
import { buildProjectOverview, buildRuntimeInfo } from "./runtime-reporting.mjs";
|
|
33
31
|
import { diagnoseRuntime as runRuntimeDiagnostics } from "./runtime-diagnostics.mjs";
|
|
32
|
+
import { bindRuntimeToolHandlers, runtimeToolHandlerNames as registeredRuntimeToolHandlerNames } from "./runtime-tool-handlers.mjs";
|
|
33
|
+
import { createRuntimeRelayConnection, normalizeRelayToolCall } from "./runtime-relay.mjs";
|
|
34
|
+
import { assertContainedPath, createRuntimeDir, redactRuntimeErrorMessage, stateRootFromProfileStatePath } from "./runtime-paths.mjs";
|
|
34
35
|
import {
|
|
35
36
|
resolveTaskCapabilities as resolveRuntimeTaskCapabilities,
|
|
36
37
|
sessionBootstrap as buildRuntimeSessionBootstrap,
|
|
37
38
|
} from "./runtime-capabilities.mjs";
|
|
38
39
|
|
|
39
|
-
const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
40
40
|
const SLOW_TOOL_CALL_MS = 30_000;
|
|
41
41
|
|
|
42
|
-
const RUNTIME_TOOL_HANDLERS = Object.freeze({
|
|
43
|
-
server_info: (runtime) => runtime.runtimeInfo(),
|
|
44
|
-
project_overview: (runtime, _args, context) => runtime.projectOverview(context),
|
|
45
|
-
session_bootstrap: (runtime, args, context) => runtime.sessionBootstrap(args, context),
|
|
46
|
-
agent_context: (runtime, args, context) => runtime.agentContextManager.agentContext(args, context),
|
|
47
|
-
resolve_task_capabilities: (runtime, args, context) => runtime.resolveTaskCapabilities(args, context),
|
|
48
|
-
list_local_skills: (runtime, args, context) => runtime.agentContextManager.listLocalSkills(args, context),
|
|
49
|
-
load_local_skill: (runtime, args, context) => runtime.agentContextManager.loadLocalSkill(args, context),
|
|
50
|
-
list_local_commands: (runtime, args, context) => runtime.agentContextManager.listLocalCommands(args, context),
|
|
51
|
-
run_local_command: (runtime, args, context) => runtime.runLocalCommand(args, context),
|
|
52
|
-
list_local_applications: (runtime, args, context) => runtime.appAutomationManager.listApplications(args, context),
|
|
53
|
-
open_local_application: (runtime, args, context) => runtime.appAutomationManager.openApplication(args, context),
|
|
54
|
-
inspect_local_application: (runtime, args, context) => runtime.appAutomationManager.inspectApplication(args, context),
|
|
55
|
-
operate_local_application: (runtime, args, context) => runtime.appAutomationManager.operateApplication(args, context),
|
|
56
|
-
browser_status: (runtime, _args, context) => runtime.browserBridgeManager.status(context),
|
|
57
|
-
pair_browser_extension: (runtime, args, context) => runtime.browserBridgeManager.pair(args, context),
|
|
58
|
-
browser_list_tabs: (runtime, args, context) => runtime.browserBridgeManager.listTabs(args, context),
|
|
59
|
-
browser_manage_tabs: (runtime, args, context) => runtime.browserBridgeManager.manageTabs(args, context),
|
|
60
|
-
browser_wait: (runtime, args, context) => runtime.browserBridgeManager.wait(args, context),
|
|
61
|
-
browser_get_source: (runtime, args, context) => runtime.browserBridgeManager.getSource(args, context),
|
|
62
|
-
browser_inspect_page: (runtime, args, context) => runtime.browserBridgeManager.inspectPage(args, context),
|
|
63
|
-
browser_action: (runtime, args, context) => runtime.browserBridgeManager.act(args, context),
|
|
64
|
-
browser_fill_form: (runtime, args, context) => runtime.browserBridgeManager.fillForm(args, context),
|
|
65
|
-
browser_screenshot: (runtime, args, context) => runtime.browserBridgeManager.screenshot(args, context),
|
|
66
|
-
browser_upload_files: (runtime, args, context) => runtime.browserBridgeManager.uploadFiles(args, context),
|
|
67
|
-
list_roots: (runtime) => runtime.listRoots(),
|
|
68
|
-
list_dir: (runtime, args, context) => runtime.listDir(args.path || ".", context),
|
|
69
|
-
list_files: (runtime, args, context) => runtime.listFiles(args.path || ".", clampInteger(args.max_files, 1000, 1, 10000), context),
|
|
70
|
-
read_file: (runtime, args, context) => runtime.readFile(args, context),
|
|
71
|
-
view_image: (runtime, args, context) => runtime.viewImage(args, context),
|
|
72
|
-
write_file: (runtime, args, context) => runtime.writeFile(args, context),
|
|
73
|
-
edit_file: (runtime, args, context) => runtime.editFile(args, context),
|
|
74
|
-
apply_patch: (runtime, args, context) => runtime.applyPatch(args, context),
|
|
75
|
-
search_text: (runtime, args, context) => runtime.searchText(args, context),
|
|
76
|
-
git_status: (runtime, args, context) => runtime.gitStatus(args, context),
|
|
77
|
-
git_diff: (runtime, args, context) => runtime.gitDiff(args, context),
|
|
78
|
-
git_log: (runtime, args, context) => runtime.gitLog(args, context),
|
|
79
|
-
git_show: (runtime, args, context) => runtime.gitShow(args, context),
|
|
80
|
-
diagnose_runtime: (runtime, _args, context) => runtime.diagnoseRuntime(context),
|
|
81
|
-
list_local_resources: (runtime) => runtime.managedJobManager.listResources(),
|
|
82
|
-
generate_ssh_key_resource: (runtime, args, context) => runtime.generateSshKeyResource(args, context),
|
|
83
|
-
stage_job: (runtime, args) => runtime.managedJobManager.stage(args),
|
|
84
|
-
start_job: (runtime, args) => runtime.managedJobManager.start(args),
|
|
85
|
-
list_jobs: (runtime, args) => runtime.managedJobManager.list(args),
|
|
86
|
-
read_job: (runtime, args) => runtime.managedJobManager.read(args),
|
|
87
|
-
cancel_job: (runtime, args) => runtime.managedJobManager.cancel(args),
|
|
88
|
-
run_process: (runtime, args, context) => runtime.runDirectProcess(args, context),
|
|
89
|
-
start_process: (runtime, args, context) => runtime.processSessionManager.start(args, context),
|
|
90
|
-
read_process: (runtime, args, context) => runtime.processSessionManager.read(args, context),
|
|
91
|
-
write_process: (runtime, args, context) => runtime.processSessionManager.write(args, context),
|
|
92
|
-
kill_process: (runtime, args, context) => runtime.processSessionManager.kill(args, context),
|
|
93
|
-
exec_command: (runtime, args, context) => runtime.execCommand(args.command, clampInteger(args.timeout_seconds, 120, 1, 600), context),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
42
|
export function runtimeToolHandlerNames() {
|
|
97
|
-
return
|
|
43
|
+
return registeredRuntimeToolHandlerNames();
|
|
98
44
|
}
|
|
99
45
|
|
|
100
46
|
export class LocalRuntime {
|
|
@@ -215,10 +161,7 @@ export class LocalRuntime {
|
|
|
215
161
|
logger: this.logger,
|
|
216
162
|
});
|
|
217
163
|
this.toolExecutor = new ToolExecutor({
|
|
218
|
-
handlers:
|
|
219
|
-
name,
|
|
220
|
-
(args, context) => handler(this, args, context),
|
|
221
|
-
])),
|
|
164
|
+
handlers: bindRuntimeToolHandlers(this),
|
|
222
165
|
policyGate: this.policyGate,
|
|
223
166
|
accountAccessGate: this.accountAccessGate,
|
|
224
167
|
callRegistry: this.callRegistry,
|
|
@@ -227,7 +170,7 @@ export class LocalRuntime {
|
|
|
227
170
|
safeMessage: (error, args) => this.safeErrorMessage(error, args),
|
|
228
171
|
slowMs: SLOW_TOOL_CALL_MS,
|
|
229
172
|
});
|
|
230
|
-
this.relay =
|
|
173
|
+
this.relay = createRuntimeRelayConnection(this, {
|
|
231
174
|
workerUrl: remoteWorkerUrl,
|
|
232
175
|
secret: remoteSecret,
|
|
233
176
|
expectedVersion: expectedRelayVersion,
|
|
@@ -662,19 +605,17 @@ export class LocalRuntime {
|
|
|
662
605
|
}
|
|
663
606
|
|
|
664
607
|
safeErrorMessage(error, toolArgs = {}) {
|
|
665
|
-
|
|
666
|
-
if (
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
return message;
|
|
608
|
+
const message = boundedErrorMessage(error);
|
|
609
|
+
if (this.policy.exposeAbsolutePaths) return message;
|
|
610
|
+
return redactRuntimeErrorMessage(message, {
|
|
611
|
+
error,
|
|
612
|
+
toolArgs,
|
|
613
|
+
workspace: this.workspace,
|
|
614
|
+
workspaceInput: this.workspaceInput,
|
|
615
|
+
runtimeDir: this.runtimeDir,
|
|
616
|
+
home: process.env.HOME || process.env.USERPROFILE || "",
|
|
617
|
+
displayPath: (value) => this.displayPath(value),
|
|
618
|
+
});
|
|
678
619
|
}
|
|
679
620
|
|
|
680
621
|
throwIfCancelled(context = {}) {
|
|
@@ -690,129 +631,6 @@ export class LocalRuntime {
|
|
|
690
631
|
}
|
|
691
632
|
}
|
|
692
633
|
|
|
693
|
-
function stateRootFromProfileStatePath(statePath) {
|
|
694
|
-
const absolute = resolve(statePath);
|
|
695
|
-
if (basename(absolute) !== "state.json") throw new Error("local resource state path is invalid");
|
|
696
|
-
const profileDir = dirname(absolute);
|
|
697
|
-
const profilesDir = dirname(profileDir);
|
|
698
|
-
if (basename(profilesDir) !== "profiles") throw new Error("local resource state path is outside the expected profile layout");
|
|
699
|
-
return dirname(profilesDir);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function assertContainedPath(root, target) {
|
|
703
|
-
const rel = relative(root, target);
|
|
704
|
-
if (rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel))) return;
|
|
705
|
-
throw new Error("path is outside the configured workspace; restart with --unrestricted-paths to allow it");
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function createRuntimeDir() {
|
|
709
|
-
const root = mkdtempSync(join(tmpdir(), "machine-bridge-mcp-"));
|
|
710
|
-
for (const name of ["home", "tmp", "cache"]) mkdirSync(join(root, name), { recursive: true, mode: 0o700 });
|
|
711
|
-
return root;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function createRelayConnection(runtime, { workerUrl, secret, expectedVersion, onFatal }) {
|
|
715
|
-
if (!workerUrl) return null;
|
|
716
|
-
return new RelayConnection({
|
|
717
|
-
workerUrl,
|
|
718
|
-
secret,
|
|
719
|
-
logger: runtime.logger,
|
|
720
|
-
maxPayload: MAX_WS_MESSAGE_BYTES,
|
|
721
|
-
expectedServer: SERVER_NAME,
|
|
722
|
-
expectedVersion: String(expectedVersion || ""),
|
|
723
|
-
helloMessage: () => ({
|
|
724
|
-
type: "hello",
|
|
725
|
-
tools: runtime.tools(),
|
|
726
|
-
policy: runtime.policy,
|
|
727
|
-
protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
|
|
728
|
-
}),
|
|
729
|
-
onMessage: (data, relayContext) => handleRelayData(runtime, data, relayContext),
|
|
730
|
-
onDisconnect: () => runtime.handleRelayDisconnect(),
|
|
731
|
-
onSuperseded: () => {
|
|
732
|
-
runtime.terminateActiveProcesses("SIGKILL");
|
|
733
|
-
runtime.processSessionManager.clear();
|
|
734
|
-
runtime.onSuperseded?.();
|
|
735
|
-
},
|
|
736
|
-
onFatal: (error) => {
|
|
737
|
-
runtime.terminateActiveProcesses("SIGKILL");
|
|
738
|
-
runtime.processSessionManager.clear();
|
|
739
|
-
onFatal?.(error);
|
|
740
|
-
},
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
function handleRelayData(runtime, data, relayContext = {}) {
|
|
746
|
-
const raw = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
|
|
747
|
-
if (Buffer.byteLength(raw) > MAX_WS_MESSAGE_BYTES) {
|
|
748
|
-
runtime.handleRelayProtocolViolation("server_message_too_large");
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
return runtime.handleMessage(raw, relayContext);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function normalizeRelayToolCall(message) {
|
|
755
|
-
const id = typeof message.id === "string" && message.id.length <= 256 ? message.id : "";
|
|
756
|
-
const tool = typeof message.tool === "string" && message.tool.length <= 128 ? message.tool : "";
|
|
757
|
-
const argumentsValue = message.arguments === undefined ? {} : message.arguments;
|
|
758
|
-
const authorization = normalizeRelayAuthorization(message.authorization);
|
|
759
|
-
if (!id || !tool || !isPlainRecord(argumentsValue) || !authorization) return { ok: false, id };
|
|
760
|
-
return {
|
|
761
|
-
ok: true,
|
|
762
|
-
id,
|
|
763
|
-
tool,
|
|
764
|
-
arguments: argumentsValue,
|
|
765
|
-
authorization,
|
|
766
|
-
timeoutMs: clampInteger(message.timeout_ms, 60_000, 1000, 610_000),
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function normalizeRelayAuthorization(value) {
|
|
771
|
-
if (!isPlainRecord(value)) return null;
|
|
772
|
-
const accountId = typeof value.account_id === "string" && /^acct_[A-Za-z0-9_-]{20,96}$/.test(value.account_id) ? value.account_id : "";
|
|
773
|
-
const accountVersion = Number(value.account_version);
|
|
774
|
-
let role;
|
|
775
|
-
try { role = normalizeAccountRole(value.role); } catch { return null; }
|
|
776
|
-
if (!accountId || !Number.isInteger(accountVersion) || accountVersion < 1) return null;
|
|
777
|
-
return Object.freeze({ account_id: accountId, account_version: accountVersion, role });
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function collectToolPathCandidates(error, toolArgs, workspace) {
|
|
781
|
-
const candidates = new Set();
|
|
782
|
-
for (const value of [error?.path, error?.dest]) if (typeof value === "string" && value) candidates.add(value);
|
|
783
|
-
const visit = (value, key = "", depth = 0) => {
|
|
784
|
-
if (depth > 5 || value === null || value === undefined) return;
|
|
785
|
-
if (typeof value === "string") {
|
|
786
|
-
if (/(?:^|[_-])(?:path|cwd|workspace|root|directory|dir)(?:$|[_-])/i.test(key) && value && !value.includes("\0")) candidates.add(value);
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
if (Array.isArray(value)) {
|
|
790
|
-
for (const item of value.slice(0, 64)) visit(item, key, depth + 1);
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
if (typeof value !== "object") return;
|
|
794
|
-
for (const [childKey, child] of Object.entries(value).slice(0, 128)) visit(child, childKey, depth + 1);
|
|
795
|
-
};
|
|
796
|
-
visit(toolArgs);
|
|
797
|
-
candidates.delete(workspace);
|
|
798
|
-
return [...candidates].sort((left, right) => right.length - left.length);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
function equivalentPathPrefixes(...values) {
|
|
802
|
-
const prefixes = new Set(values.filter(Boolean).map((value) => String(value)));
|
|
803
|
-
for (const value of [...prefixes]) {
|
|
804
|
-
if (value.startsWith("/private/")) prefixes.add(value.slice("/private".length));
|
|
805
|
-
else if (value.startsWith("/") && ["/var/", "/tmp/", "/etc/"].some((prefix) => value.startsWith(prefix))) prefixes.add(`/private${value}`);
|
|
806
|
-
}
|
|
807
|
-
return [...prefixes].sort((left, right) => right.length - left.length);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
function replacePathPrefix(message, pathValue, replacement) {
|
|
811
|
-
if (!pathValue) return message;
|
|
812
|
-
const normalized = String(pathValue);
|
|
813
|
-
return message.split(normalized).join(replacement);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
634
|
function shortCallId(value) {
|
|
817
635
|
return String(value || "").slice(0, 20);
|
|
818
636
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
3
|
+
|
|
4
|
+
const WINDOWS_LAUNCHER = "service-launcher.cmd";
|
|
5
|
+
const WINDOWS_TASK_COMMAND_MAX_CHARS = 262;
|
|
6
|
+
|
|
7
|
+
export function windowsLauncherPath(stateRoot) {
|
|
8
|
+
return path.join(path.resolve(String(stateRoot)), WINDOWS_LAUNCHER);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function writeWindowsLauncher(spec) {
|
|
12
|
+
const launcherPath = windowsLauncherPath(spec.stateRoot);
|
|
13
|
+
const content = windowsLauncherContent(spec);
|
|
14
|
+
replaceFileAtomicallySync(launcherPath, content, { mode: 0o600 });
|
|
15
|
+
return { path: launcherPath, content };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function windowsLauncherContent(spec) {
|
|
19
|
+
const command = [spec.node, ...(spec.daemonArgs || [])].map(windowsBatchArgument).join(" ");
|
|
20
|
+
const stdout = windowsBatchArgument(spec.stdout);
|
|
21
|
+
const stderr = windowsBatchArgument(spec.stderr);
|
|
22
|
+
return [
|
|
23
|
+
"@echo off",
|
|
24
|
+
"setlocal DisableDelayedExpansion",
|
|
25
|
+
":restart",
|
|
26
|
+
`${command} 1>>${stdout} 2>>${stderr}`,
|
|
27
|
+
'set "mbm_exit=%ERRORLEVEL%"',
|
|
28
|
+
'if "%mbm_exit%"=="0" exit /b 0',
|
|
29
|
+
'"%SystemRoot%\\System32\\timeout.exe" /t 5 /nobreak >nul 2>&1',
|
|
30
|
+
"goto restart",
|
|
31
|
+
"",
|
|
32
|
+
].join("\r\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function windowsTaskAction(launcherPath) {
|
|
36
|
+
const action = String(launcherPath);
|
|
37
|
+
if (action.includes("\0") || /[\r\n]/.test(action)) throw new Error("Windows autostart launcher path contains a prohibited control character");
|
|
38
|
+
if (!path.isAbsolute(action) && !path.win32.isAbsolute(action)) throw new Error("Windows autostart launcher path must be absolute");
|
|
39
|
+
if (action.includes("%")) throw new Error("Windows autostart launcher path must not contain a percent sign because Task Scheduler may expand it as an environment variable");
|
|
40
|
+
if (action.length > WINDOWS_TASK_COMMAND_MAX_CHARS) {
|
|
41
|
+
throw new Error(`Windows autostart action exceeds the ${WINDOWS_TASK_COMMAND_MAX_CHARS}-character Task Scheduler limit; use the default state directory or a shorter --state-dir`);
|
|
42
|
+
}
|
|
43
|
+
return action;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function windowsCommandLineArgument(value) {
|
|
47
|
+
const text = String(value);
|
|
48
|
+
if (text.includes("\0")) throw new Error("Windows command-line argument contains a NUL byte");
|
|
49
|
+
if (/[\r\n]/.test(text)) throw new Error("Windows command-line argument contains a line break");
|
|
50
|
+
const escapedQuotes = text.replace(/(\\*)"/g, (_match, slashes) => `${slashes}${slashes}\\"`);
|
|
51
|
+
const escapedTrailingSlashes = escapedQuotes.replace(/(\\+)$/, slashes => `${slashes}${slashes}`);
|
|
52
|
+
return `"${escapedTrailingSlashes}"`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function windowsBatchArgument(value) {
|
|
56
|
+
return windowsCommandLineArgument(value).replaceAll("%", "%%");
|
|
57
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
3
1
|
import { waitForInactiveStatus } from "./service-convergence.mjs";
|
|
2
|
+
import {
|
|
3
|
+
windowsLauncherPath, windowsTaskAction, writeWindowsLauncher,
|
|
4
|
+
} from "./windows-launcher.mjs";
|
|
5
|
+
export {
|
|
6
|
+
windowsBatchArgument, windowsCommandLineArgument, windowsLauncherContent,
|
|
7
|
+
windowsLauncherPath, windowsTaskAction, writeWindowsLauncher,
|
|
8
|
+
} from "./windows-launcher.mjs";
|
|
4
9
|
|
|
5
10
|
export const WINDOWS_TASK = "MachineBridgeMCP";
|
|
6
|
-
const WINDOWS_LAUNCHER = "service-launcher.cmd";
|
|
7
|
-
const WINDOWS_TASK_COMMAND_MAX_CHARS = 262;
|
|
8
11
|
const WINDOWS_STATUS_SCRIPT = [
|
|
9
12
|
"$task = Get-ScheduledTask -TaskName 'MachineBridgeMCP' -ErrorAction SilentlyContinue;",
|
|
10
13
|
"if ($null -eq $task) { exit 3 };",
|
|
@@ -150,58 +153,6 @@ export async function statusWindowsTask(options = {}) {
|
|
|
150
153
|
};
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
export function windowsLauncherPath(stateRoot) {
|
|
154
|
-
return path.join(path.resolve(String(stateRoot)), WINDOWS_LAUNCHER);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function writeWindowsLauncher(spec) {
|
|
158
|
-
const launcherPath = windowsLauncherPath(spec.stateRoot);
|
|
159
|
-
const content = windowsLauncherContent(spec);
|
|
160
|
-
replaceFileAtomicallySync(launcherPath, content, { mode: 0o600 });
|
|
161
|
-
return { path: launcherPath, content };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function windowsLauncherContent(spec) {
|
|
165
|
-
const command = [spec.node, ...(spec.daemonArgs || [])].map(windowsBatchArgument).join(" ");
|
|
166
|
-
const stdout = windowsBatchArgument(spec.stdout);
|
|
167
|
-
const stderr = windowsBatchArgument(spec.stderr);
|
|
168
|
-
return [
|
|
169
|
-
"@echo off",
|
|
170
|
-
"setlocal DisableDelayedExpansion",
|
|
171
|
-
":restart",
|
|
172
|
-
`${command} 1>>${stdout} 2>>${stderr}`,
|
|
173
|
-
'set "mbm_exit=%ERRORLEVEL%"',
|
|
174
|
-
'if "%mbm_exit%"=="0" exit /b 0',
|
|
175
|
-
'"%SystemRoot%\\System32\\timeout.exe" /t 5 /nobreak >nul 2>&1',
|
|
176
|
-
"goto restart",
|
|
177
|
-
"",
|
|
178
|
-
].join("\r\n");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function windowsTaskAction(launcherPath) {
|
|
182
|
-
const action = String(launcherPath);
|
|
183
|
-
if (action.includes("\0") || /[\r\n]/.test(action)) throw new Error("Windows autostart launcher path contains a prohibited control character");
|
|
184
|
-
if (!path.isAbsolute(action) && !path.win32.isAbsolute(action)) throw new Error("Windows autostart launcher path must be absolute");
|
|
185
|
-
if (action.includes("%")) throw new Error("Windows autostart launcher path must not contain a percent sign because Task Scheduler may expand it as an environment variable");
|
|
186
|
-
if (action.length > WINDOWS_TASK_COMMAND_MAX_CHARS) {
|
|
187
|
-
throw new Error(`Windows autostart action exceeds the ${WINDOWS_TASK_COMMAND_MAX_CHARS}-character Task Scheduler limit; use the default state directory or a shorter --state-dir`);
|
|
188
|
-
}
|
|
189
|
-
return action;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function windowsCommandLineArgument(value) {
|
|
193
|
-
const text = String(value);
|
|
194
|
-
if (text.includes("\0")) throw new Error("Windows command-line argument contains a NUL byte");
|
|
195
|
-
if (/[\r\n]/.test(text)) throw new Error("Windows command-line argument contains a line break");
|
|
196
|
-
const escapedQuotes = text.replace(/(\\*)"/g, (_match, slashes) => `${slashes}${slashes}\\"`);
|
|
197
|
-
const escapedTrailingSlashes = escapedQuotes.replace(/(\\+)$/, slashes => `${slashes}${slashes}`);
|
|
198
|
-
return `"${escapedTrailingSlashes}"`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function windowsBatchArgument(value) {
|
|
202
|
-
return windowsCommandLineArgument(value).replaceAll("%", "%%");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
156
|
function completedSince(before, after) {
|
|
206
157
|
return after?.installed === true
|
|
207
158
|
&& after.active === false
|