Files
openclaw/extensions/mxc/src/windows-command.ts
Paul Campbell 008f04a656 feat(mxc): add Windows MXC sandbox backend (#97086)
* feat(mxc): add Windows MXC sandbox backend

Add the official MXC sandbox plugin package with Windows ProcessContainer execution, plugin-owned MXC SDK dependency packaging, host-backed filesystem bridge support, and configured MXC policy file loading via mxcPolicyPaths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(mxc): preserve Windows binary override paths

* fix: remove stray sandbox barrel export

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9ea19539-b8ca-44fb-93bd-b8496e3deb2c

* fix(mxc): address sandbox review feedback

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(mxc): satisfy test type checks

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(mxc): clarify protected skill enforcement

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test(mxc): align fail-closed expectations

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(mxc): satisfy extension lint

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(plugin-sdk): narrow fs-safe remove surface

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(mxc): repair rebased CI failures

* fix(scripts): declare shrinkwrap override normalizer

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Gio Della-Libera <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: Dallin Romney <dallinromney@gmail.com>
2026-07-12 23:07:25 -07:00

66 lines
2.9 KiB
TypeScript

// Windows command-line construction and cmd.exe quoting for the MXC
// ProcessContainer backend.
//
// Security note: `command` and `args` originate from the agent/tool layer and
// are embedded into MXC's `process.commandLine`, which MXC passes to
// CreateProcessInSandbox -> cmd.exe *inside* the AppContainer sandbox. They
// never reach a host shell and cannot influence the base64 sandbox-policy
// payload (that travels on a separate argv flag to wxc-exec). Quoting here is
// about delivering arguments to the sandboxed program intact and preventing
// `%`-expansion / quote-breakout within the inner cmd.exe command line.
import { randomBytes } from "node:crypto";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
function resolveCmdShell(): string {
return process.env.ComSpec?.trim() || "cmd.exe";
}
// `cmd.exe /d /s /c "<line>"`: `/d` skips AutoRun, `/s` makes cmd.exe strip
// exactly one outer quote pair without re-parsing the inner line, which lets the
// wrapped script keep embedded `"` chars.
export function buildCommandLine(commandScript: string, args: readonly string[]): string {
const shell = resolveCmdShell();
if (args.length === 0) {
return `${shell} /d /s /c "${commandScript}"`;
}
const escapedArgs = args.map(cmdArgumentEscape).join(" ");
return `${shell} /d /s /c "${cmdArgumentEscape(commandScript)} ${escapedArgs}"`;
}
// Quote a single cmd.exe token: wrap in double quotes and neutralize the chars
// that would otherwise break out of the quoted token or trigger expansion:
// `"` -> `""` (cmd's in-quote quote escape), `%` -> `%%` (suppress env
// expansion), `^` -> `^^` (caret escape) so the token survives cmd parsing.
function cmdArgumentEscape(value: string): string {
return `"${value.replaceAll("^", "^^").replaceAll("%", "%%").replaceAll(`"`, `""`)}"`;
}
// When positional args accompany a script, cmd.exe cannot reliably receive both
// an inline script and argv. Materialize the script as a temporary `.cmd` file
// (exclusive-create, owner-only mode) and run that file with the args appended.
// The caller must invoke `cleanup()` once the command has been built.
export function createWindowsCommandBridge(params: {
args: readonly string[] | undefined;
script: string;
tempDir: string;
}): { command: string; cleanup: () => void } {
if (!params.args || params.args.length === 0) {
return { command: params.script, cleanup: () => {} };
}
const bridgeDir = mkdtempSync(path.join(params.tempDir, ".openclaw-mxc-cmd-"));
const commandFile = path.join(bridgeDir, `${randomBytes(8).toString("hex")}.cmd`);
try {
writeFileSync(commandFile, `@echo off\r\n${params.script}`, { flag: "wx", mode: 0o600 });
} catch (err) {
rmSync(bridgeDir, { force: true, recursive: true });
throw err;
}
return {
command: commandFile,
cleanup: () => rmSync(bridgeDir, { force: true, recursive: true }),
};
}