Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 42 additions & 10 deletions extensions/mxc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,52 @@ readiness behavior to change as MXC host support matures.
and out-of-range values fail plugin activation with an actionable error
(`Invalid mxc plugin config: <reason>`) instead of falling back silently.

| Field | Type | Default | Notes |
| ---------------- | --------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mxcBinaryPath` | `string` | unset | Non-empty override for the `wxc-exec.exe` executor path; see [SDK-only executor discovery](#supported). |
| `containment` | `"process" \| "processcontainer"` | `"process"` | Both currently resolve to Windows ProcessContainer. |
| `network` | `"none" \| "default"` | `"none"` | `"default"` allows outbound network via the `internetClient` capability. |
| `timeoutSeconds` | `number` | unset (baseline default `300` applies) | Must be `>= 1` and `<= 2147000` (the largest Node-safe `setTimeout` delay in whole seconds). Capped to the sandbox policy baseline timeout when both are set. |
| `debug` | `boolean` | `false` | Forwards debug output from the MXC SDK launcher. |
| `mxcPolicyPaths` | `string[]` | unset (built-in baseline only) | Every entry must be a non-empty absolute path. See [Sandbox policy files](#sandbox-policy-files). |
| Field | Type | Default | Notes |
| ---------------- | ------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `securityLevel` | `"Locked Down" \| "Recommended" \| "Unprotected"` | `"Recommended"` | Windows-aligned baseline for network, standard folders, clipboard, and timeout. |
| `mxcBinaryPath` | `string` | unset | Non-empty override for the `wxc-exec.exe` executor path; see [SDK-only executor discovery](#supported). |
| `containment` | `"process" \| "processcontainer"` | `"process"` | Both currently resolve to Windows ProcessContainer. |
| `network` | `"none" \| "default"` | selected preset | Optional restrictive override. `"none"` blocks outbound network; `"default"` retains the preset policy and cannot weaken `Locked Down`. |
| `timeoutSeconds` | `number` | selected preset | Optional preset override from 1 through 2147000 seconds. The sandbox policy baseline can enforce a shorter timeout. |
| `debug` | `boolean` | `false` | Forwards debug output from the MXC SDK launcher. |
| `mxcPolicyPaths` | `string[]` | built-in baseline only | Every entry must be a non-empty absolute path. See [Sandbox policy files](#sandbox-policy-files). |

Any other key is rejected. `openclaw.plugin.json` publishes the same schema
(enums, `minimum`/`maximum` bounds) so `openclaw config` validation and CLI
help stay in sync with plugin runtime validation.

### Security levels

| Security level | Internet | Gateway identity's Documents / Downloads / Desktop | Clipboard | Timeout |
| -------------- | -------- | -------------------------------------------------- | ---------- | ----------- |
| `Locked Down` | Off | None | None | 30 seconds |
| `Recommended` | On | Read-only | Read | 60 seconds |
| `Unprotected` | On | Read-write | Read-write | 300 seconds |

Standard-folder grants use the Windows known-folder locations for the identity
running the Gateway and honor folder redirection. The active workspace and MXC
runtime paths remain available independently of the selected preset. When a
writable workspace is inside one of those folders, the workspace grant takes
precedence and MXC omits the overlapping automatic read-only folder grant.

All presets disable input injection and desktop system control. `Locked Down`
keeps ProcessContainer UI isolation. Clipboard-enabled presets use desktop UI
isolation so their clipboard policy can reach the clipboard in the Gateway's
Windows session; the top-level clipboard setting still independently enforces
read-only or read-write access.

Explicit `network: "none"` can tighten a clipboard-enabled preset, while
`network: "default"` cannot enable network for `Locked Down`.
`timeoutSeconds` overrides the preset timeout, subject to a shorter policy-file
ceiling. Policy files can also add explicit filesystem paths.

## Supported

- Windows hosts with the MXC executor installed through `@microsoft/mxc-sdk`.
- Explicit opt-in after plugin install with `sandbox.backend: "mxc"`.
- MXC `process` containment, which resolves to Windows ProcessContainer.
- Windows-aligned `Locked Down`, `Recommended`, and `Unprotected` security
presets.
- `workspaceAccess`:
- `none`: only the isolated sandbox workdir is mounted, read-only. There is
no separate mount for the real agent workspace.
Expand Down Expand Up @@ -257,8 +285,12 @@ agents may still use them.

## Host readiness

IsoEnvBroker must be available on the host OS. The plugin checks this before
registering the sandbox backend.
The plugin validates that its packaged MXC executor is present before
registering the sandbox backend. The MXC `process` backend probes the available
Windows containment tier at execution time and falls back from BaseContainer to
its supported AppContainer path when needed. It does not require the legacy
`IsoEnvBroker` service name; current SF2 builds expose the separate
`IsolationSession` services instead.

Host preparation is advisory. If directory listing inside the sandbox fails with
`Access is denied`, run this once from an elevated prompt:
Expand Down
16 changes: 16 additions & 0 deletions extensions/mxc/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,20 @@ describe("mxc plugin entry", () => {
it("wires the runtime config schema into the plugin entry and manifest", () => {
expect(plugin.configSchema?.jsonSchema).toEqual(manifest.configSchema);
});

it("publishes the security preset contract", () => {
expect(manifest.configSchema.properties.securityLevel).toMatchObject({
enum: ["Locked Down", "Recommended", "Unprotected"],
default: "Recommended",
});
expect(manifest.configContracts.dangerousFlags).toContainEqual({
path: "securityLevel",
equals: "Unprotected",
});
expect(manifest.uiHints.securityLevel).toMatchObject({
label: "Default sandbox security",
});
expect(manifest.uiHints.securityLevel).not.toHaveProperty("advanced");
expect(manifest.uiHints.network.advanced).toBe(true);
});
});
26 changes: 20 additions & 6 deletions extensions/mxc/openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
"type": "object",
"additionalProperties": false,
"properties": {
"securityLevel": {
"type": "string",
"enum": ["Locked Down", "Recommended", "Unprotected"],
"default": "Recommended",
"description": "Windows-aligned baseline for network, standard folders, clipboard, and timeout."
},
"mxcBinaryPath": {
"type": "string",
"minLength": 1,
Expand All @@ -23,13 +29,13 @@
"network": {
"type": "string",
"enum": ["none", "default"],
"description": "Outbound network policy. 'none' blocks all network; 'default' allows outbound access via the internetClient capability."
"description": "Optional restrictive preset override. 'none' blocks all network; 'default' retains the selected preset's outbound policy."
},
"timeoutSeconds": {
"type": "number",
"minimum": 1,
"maximum": 2147000,
"description": "Per-command execution timeout in seconds. Capped to the sandbox policy baseline timeout when both are set."
"description": "Optional preset timeout override in seconds. Capped to the sandbox policy baseline timeout when both are set."
},
"debug": {
"type": "boolean",
Expand All @@ -46,9 +52,16 @@
}
},
"configContracts": {
"dangerousFlags": [{ "path": "network", "equals": "default" }]
"dangerousFlags": [
{ "path": "securityLevel", "equals": "Unprotected" },
{ "path": "network", "equals": "default" }
]
},
"uiHints": {
"securityLevel": {
"label": "Default sandbox security",
"help": "Recommended is intended for normal OpenClaw use. Locked Down disables internet and clipboard and adds no standard-folder grants. Unprotected grants broader filesystem and clipboard access but does not disable MXC containment."
},
"mxcBinaryPath": {
"label": "MXC Executor Path",
"help": "Optional absolute path to the MXC executor (wxc-exec.exe). Leave unset to auto-discover from the installed @microsoft/mxc-sdk.",
Expand All @@ -60,11 +73,12 @@
},
"network": {
"label": "Network Policy",
"help": "'none' blocks all outbound network; 'default' allows outbound access via the internetClient capability, which weakens sandbox isolation."
"help": "Optional restrictive override for the selected security preset. 'none' blocks all outbound network; 'default' retains the preset policy and cannot enable network for Locked Down.",
"advanced": true
},
"timeoutSeconds": {
"label": "Command Timeout (seconds)",
"help": "Per-command execution timeout, from 1 to 2147000 seconds. Capped to the sandbox policy baseline timeout when both are set.",
"label": "Command Timeout Override (seconds)",
"help": "Optional override for the selected security preset, from 1 to 2147000 seconds. Capped to the sandbox policy baseline timeout when both are set.",
"advanced": true
},
"debug": {
Expand Down
39 changes: 25 additions & 14 deletions extensions/mxc/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import {
} from "openclaw/plugin-sdk/extension-shared";
import { MAX_TIMER_TIMEOUT_SECONDS } from "openclaw/plugin-sdk/number-runtime";
import { z } from "zod";
import {
DEFAULT_MXC_SECURITY_LEVEL,
getMxcSecurityPreset,
MXC_SECURITY_LEVELS,
type MxcSecurityLevel,
} from "./security-level.js";

const MXC_CONTAINMENTS = ["process", "processcontainer"] as const;
const MXC_NETWORK_MODES = ["none", "default"] as const;
Expand All @@ -16,23 +22,27 @@ type MxcNetworkMode = (typeof MXC_NETWORK_MODES)[number];

export type MxcConfig = {
mxcBinaryPath?: string;
securityLevel: MxcSecurityLevel;
containment: MxcContainment;
network: MxcNetworkMode;
timeoutSeconds: number;
timeoutSecondsConfigured?: boolean;
debug: boolean;
mxcPolicyPaths?: string[];
};

const DEFAULT_CONTAINMENT: MxcContainment = "process";
const DEFAULT_NETWORK: MxcNetworkMode = "none";
const DEFAULT_TIMEOUT_SECONDS = 120;
const DEFAULT_DEBUG = false;

const nonEmptyTrimmedString = (message: string) =>
z.string({ error: message }).trim().min(1, { error: message });

const MxcPluginConfigSchema = z.strictObject({
securityLevel: z
.enum(MXC_SECURITY_LEVELS, {
error: `securityLevel must be one of ${MXC_SECURITY_LEVELS.join(", ")}`,
})
.describe("Windows-aligned baseline for network, standard folders, clipboard, and timeout.")
.default(DEFAULT_MXC_SECURITY_LEVEL),
mxcBinaryPath: nonEmptyTrimmedString("mxcBinaryPath must be a non-empty string")
.describe(
"Absolute path to the MXC executor (wxc-exec.exe). When unset, the executor is discovered from the installed @microsoft/mxc-sdk.",
Expand All @@ -51,7 +61,7 @@ const MxcPluginConfigSchema = z.strictObject({
error: `network must be one of ${MXC_NETWORK_MODES.join(", ")}`,
})
.describe(
"Outbound network policy. 'none' blocks all network; 'default' allows outbound access via the internetClient capability.",
"Optional restrictive preset override. 'none' blocks all network; 'default' retains the selected preset's outbound policy.",
)
.optional(),
timeoutSeconds: z
Expand All @@ -63,7 +73,7 @@ const MxcPluginConfigSchema = z.strictObject({
error: `timeoutSeconds must be a number <= ${MAX_TIMER_TIMEOUT_SECONDS}`,
})
.describe(
"Per-command execution timeout in seconds. Capped to the sandbox policy baseline timeout when both are set.",
"Optional preset timeout override in seconds. Capped to the sandbox policy baseline timeout when both are set.",
)
.optional(),
debug: z
Expand Down Expand Up @@ -102,11 +112,13 @@ export function createMxcPluginConfigSchema(): OpenClawPluginConfigSchema {

export function resolveConfig(value: unknown): MxcConfig {
if (value === undefined) {
const preset = getMxcSecurityPreset(DEFAULT_MXC_SECURITY_LEVEL);
return {
mxcBinaryPath: undefined,
securityLevel: DEFAULT_MXC_SECURITY_LEVEL,
containment: DEFAULT_CONTAINMENT,
network: DEFAULT_NETWORK,
timeoutSeconds: DEFAULT_TIMEOUT_SECONDS,
network: preset.networkEnabled ? "default" : "none",
timeoutSeconds: preset.timeoutSeconds,
debug: DEFAULT_DEBUG,
};
}
Expand All @@ -118,19 +130,18 @@ export function resolveConfig(value: unknown): MxcConfig {
}

const config = parsed.data;
const securityLevel = config.securityLevel ?? DEFAULT_MXC_SECURITY_LEVEL;
const preset = getMxcSecurityPreset(securityLevel);
const presetNetwork: MxcNetworkMode = preset.networkEnabled ? "default" : "none";
const resolved: MxcConfig = {
mxcBinaryPath: config.mxcBinaryPath,
securityLevel,
containment: config.containment ?? DEFAULT_CONTAINMENT,
network: config.network ?? DEFAULT_NETWORK,
timeoutSeconds: config.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS,
network: presetNetwork === "none" ? "none" : (config.network ?? presetNetwork),
timeoutSeconds: config.timeoutSeconds ?? preset.timeoutSeconds,
debug: config.debug ?? DEFAULT_DEBUG,
mxcPolicyPaths: resolveMxcPolicyPaths(config.mxcPolicyPaths),
};

if (config.timeoutSeconds !== undefined) {
resolved.timeoutSecondsConfigured = true;
}

return resolved;
}

Expand Down
2 changes: 1 addition & 1 deletion extensions/mxc/src/mxc-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,9 @@ export function createMxcSandboxBackendHandle(params: {
// Shell commands use a restrictive policy (no network, 30s timeout)
const restrictiveConfig: MxcConfig = {
...params.config,
securityLevel: "Locked Down",
network: "none",
timeoutSeconds: 30,
timeoutSecondsConfigured: true,
};
const effectiveWorkdir = path.resolve(params.workdir);
const workspaceAccess = params.workspaceAccess ?? "rw";
Expand Down
43 changes: 33 additions & 10 deletions extensions/mxc/src/mxc-container-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import type {
LoadedSandboxBaselinePolicy,
SandboxConfiguredPathEntry,
} from "./sandbox-policy-loader.js";
import { getMxcSecurityPreset, type MxcStandardFolderAccess } from "./security-level.js";
import { buildCommandLine } from "./windows-command.js";
import { normalizeWindowsProcessEnvRecord } from "./windows-env.js";
import { resolveWindowsStandardFolders } from "./windows-known-folders.js";
import {
resolveMxcReadOnlySkillMounts,
type MxcReadOnlySkillMount,
Expand Down Expand Up @@ -107,10 +109,12 @@ export function buildMxcContainerConfig(params: {
env: Record<string, string>;
}): ContainerConfig {
const networkAllowed = params.config.network === "default";
const preset = getMxcSecurityPreset(params.config.securityLevel);
const filesystem = buildFilesystemConfig({
baseline: params.baseline,
context: params.baselineContext,
sandboxTempDir: params.sandboxTempDir,
standardFolderAccess: preset.standardFolderAccess,
workspace: params.workspace,
});

Expand All @@ -133,8 +137,8 @@ export function buildMxcContainerConfig(params: {
},
filesystem,
ui: {
disable: true,
clipboard: "none",
disable: false,
clipboard: preset.clipboard,
injection: false,
},
network: {
Expand All @@ -146,7 +150,7 @@ export function buildMxcContainerConfig(params: {
leastPrivilege: true,
capabilities: networkAllowed ? ["internetClient"] : [],
ui: {
isolation: "container",
isolation: preset.clipboard === "none" ? "container" : "desktop",
desktopSystemControl: false,
systemSettings: "none",
ime: false,
Expand All @@ -159,9 +163,18 @@ function buildFilesystemConfig(params: {
baseline: LoadedSandboxBaselinePolicy;
context: BaselineApplicationContext;
sandboxTempDir: string;
standardFolderAccess: MxcStandardFolderAccess;
workspace: MxcWorkspaceContext;
}): MxcFilesystemConfig {
const readwritePathSpecs = resolveWorkspaceReadwritePathSpecs(params.workspace);
const standardFolders = Object.values(resolveWindowsStandardFolders()).filter(
(folder): folder is string => typeof folder === "string",
);
const readwritePathSpecs = [
...resolveWorkspaceReadwritePathSpecs(params.workspace),
...(params.standardFolderAccess === "readwrite"
? standardFolders.map(optionalFilesystemPath)
: []),
];
const readonlyPathSpecs = [
...resolveWorkspaceReadonlyPathSpecs(params.workspace),
...resolveBaselineReadonlyPathSpecs(params.baseline, params.context),
Expand All @@ -181,16 +194,27 @@ function buildFilesystemConfig(params: {
);
}

const readwritePaths = resolveExistingFilesystemPaths(readwritePathSpecs, "readwrite");
if (params.standardFolderAccess === "readonly") {
readonlyPathSpecs.push(
...standardFolders
.filter(
(standardFolder) =>
!readwritePaths.some((readwritePath) => pathsOverlap(readwritePath, standardFolder)),
)
.map(optionalFilesystemPath),
);
}

const protectedSkillPolicyPaths = resolveMxcProtectedSkillPolicyPaths(params.workspace);
// ProcessContainer writable-parent grants override nested read-only grants.
// Fail closed instead of claiming protected skill overlays are enforceable.
assertNoMxcReadwriteReadonlyOverlap({
readwritePaths: resolveExistingFilesystemPaths(readwritePathSpecs, "readwrite"),
readwritePaths,
readonlyPaths: protectedSkillPolicyPaths,
});

const readonlyPaths = resolveExistingFilesystemPaths(readonlyPathSpecs, "read-only");
const readwritePaths = resolveExistingFilesystemPaths(readwritePathSpecs, "readwrite");
assertNoMxcReadwriteReadonlyOverlap({ readwritePaths, readonlyPaths });

return {
Expand Down Expand Up @@ -364,10 +388,9 @@ function resolveProcessTimeoutSeconds(
config: MxcConfig,
baseline: LoadedSandboxBaselinePolicy,
): number {
if (config.timeoutSecondsConfigured === true) {
return Math.min(config.timeoutSeconds, baseline.process.timeoutSeconds);
}
return baseline.process.timeoutSeconds;
return baseline.process.timeoutSecondsConfigured
? Math.min(config.timeoutSeconds, baseline.process.timeoutSeconds)
: config.timeoutSeconds;
}

function assertNoMxcReadwriteReadonlyOverlap(params: {
Expand Down
Loading
Loading