Files
openclaw/extensions/mxc/src/windows-env.ts
T
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

120 lines
3.6 KiB
TypeScript

// Windows process environment helpers for the MXC ProcessContainer backend.
//
// MXC's BaseContainerRunner treats a non-empty `process.env` as a replacement
// environment block for CreateProcessInSandbox: when present it replaces the
// entire default OS environment, so any required OS var (SystemRoot, COMSPEC, …)
// that is not listed is missing and cmd.exe fails with ERROR_ENVVAR_NOT_FOUND.
// These helpers build a minimal-but-complete Windows env block from required OS
// defaults plus caller overrides, and a separate launcher env for the spawn
// process itself.
// Env vars required by cmd.exe / CreateProcess inside the AppContainer. Caller
// overrides are layered on top; nothing else from the host environment leaks in.
const WINDOWS_PROCESS_ENV_DEFAULT_KEYS = [
"SystemRoot",
"SystemDrive",
"ComSpec",
"WINDIR",
"PATH",
"PATHEXT",
"TEMP",
"TMP",
"USERPROFILE",
"APPDATA",
"LOCALAPPDATA",
"ProgramData",
"ALLUSERSPROFILE",
"ProgramFiles",
"ProgramFiles(x86)",
"ProgramW6432",
"CommonProgramFiles",
"CommonProgramFiles(x86)",
"CommonProgramW6432",
"PUBLIC",
"HOMEDRIVE",
"HOMEPATH",
"USERNAME",
"USERDOMAIN",
"COMPUTERNAME",
"OS",
"PROCESSOR_ARCHITECTURE",
"PROCESSOR_IDENTIFIER",
"PROCESSOR_LEVEL",
"PROCESSOR_REVISION",
"NUMBER_OF_PROCESSORS",
] as const;
// Env vars forwarded to the plugin-side Node launcher process (not the sandboxed
// child). The launcher only needs enough OS context to locate Node, temp dirs,
// and the user profile; the sandbox policy itself travels via the JSON payload.
const LAUNCHER_ENV_KEYS = [
"SystemRoot",
"SystemDrive",
"ComSpec",
"WINDIR",
"PATH",
"PATHEXT",
"TEMP",
"TMP",
"USERPROFILE",
"APPDATA",
"LOCALAPPDATA",
"ProgramFiles",
"ProgramFiles(x86)",
"ProgramW6432",
] as const;
function getEnvValueCaseInsensitive(env: NodeJS.ProcessEnv, key: string): string | undefined {
const exact = env[key];
if (exact !== undefined) {
return exact;
}
const normalizedKey = key.toLowerCase();
const match = Object.entries(env).find(
([candidate]) => candidate.toLowerCase() === normalizedKey,
);
return match?.[1];
}
function setCaseInsensitiveEnvEntry(
entries: Map<string, { key: string; value: string }>,
key: string,
value: string | undefined,
): void {
if (!key || key.includes("=") || value === undefined) {
return;
}
entries.set(key.toLowerCase(), { key, value });
}
// Build the Windows replacement env block: required OS defaults first, then
// caller overrides (case-insensitively, since Windows env names are
// case-insensitive). Keys containing `=` are dropped so a malicious caller key
// cannot inject extra `NAME=VALUE` pairs into the block.
export function normalizeWindowsProcessEnvRecord(
callerEnv: Record<string, string>,
hostEnv: NodeJS.ProcessEnv = process.env,
): string[] {
const entries = new Map<string, { key: string; value: string }>();
for (const key of WINDOWS_PROCESS_ENV_DEFAULT_KEYS) {
setCaseInsensitiveEnvEntry(entries, key, getEnvValueCaseInsensitive(hostEnv, key));
}
for (const [key, value] of Object.entries(callerEnv)) {
setCaseInsensitiveEnvEntry(entries, key, value);
}
return [...entries.values()]
.toSorted((a, b) => a.key.localeCompare(b.key))
.map(({ key, value }) => `${key}=${value}`);
}
export function buildLauncherEnv(hostEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {};
for (const key of LAUNCHER_ENV_KEYS) {
const value = getEnvValueCaseInsensitive(hostEnv, key);
if (value !== undefined) {
env[key] = value;
}
}
return env;
}