@openclaw/mxc-sandbox 2026.9.8 → 2026.10.1-beta.2

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.
Files changed (3) hide show
  1. package/README.md +32 -5
  2. package/dist/index.js +126 -202
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -38,7 +38,9 @@ readiness behavior to change as MXC host support matures.
38
38
 
39
39
  - Plugin id: `mxc`
40
40
  - Package: `@openclaw/mxc-sandbox`
41
- - Minimum OpenClaw host: `2026.6.11`
41
+ - Minimum OpenClaw install host: `2026.6.11`
42
+ - Plugin API host requirement: `2026.9.6` or newer. An older host cannot run
43
+ the current plugin even if the MXC executor is updated.
42
44
 
43
45
  ## Plugin config
44
46
 
@@ -48,7 +50,7 @@ and out-of-range values fail plugin activation with an actionable error
48
50
 
49
51
  | Field | Type | Default | Notes |
50
52
  | ---------------- | --------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
- | `mxcBinaryPath` | `string` | unset | Non-empty override for the `wxc-exec.exe` executor path; see [SDK-only executor discovery](#supported). |
53
+ | `mxcBinaryPath` | `string` | unset | Non-empty override for a compatible `wxc-exec.exe` supporting `--probe`; see [Host readiness](#host-readiness). |
52
54
  | `containment` | `"process" \| "processcontainer"` | `"process"` | Both currently resolve to Windows ProcessContainer. |
53
55
  | `network` | `"none" \| "default"` | `"none"` | `"default"` allows outbound network via the `internetClient` capability. |
54
56
  | `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. |
@@ -86,7 +88,7 @@ help stay in sync with plugin runtime validation.
86
88
  - OpenClaw passes per-run command, environment, and filesystem config to the
87
89
  plugin's Node launcher through a short-lived local payload file, and deletes
88
90
  that file and its temp directory when the launcher or run finishes.
89
- - `@microsoft/mxc-sdk@0.7.0` then carries the full base64 request envelope on
91
+ - `@microsoft/mxc-sdk@0.8.0` then carries the full base64 request envelope on
90
92
  the native `wxc-exec` process argv. A host user with process-inspection rights
91
93
  can observe that command, environment, and policy data while the process is
92
94
  running. Do not put secrets in MXC command arguments or environment values
@@ -257,8 +259,33 @@ agents may still use them.
257
259
 
258
260
  ## Host readiness
259
261
 
260
- IsoEnvBroker must be available on the host OS. The plugin checks this before
261
- registering the sandbox backend.
262
+ Before registering the sandbox backend, the plugin runs the MXC executor's own
263
+ host probe (`wxc-exec --probe`) and requires it to select an isolation tier
264
+ (`base-container`, `appcontainer-bfs`, or `appcontainer-dacl`). It probes the
265
+ same executor it launches, including an `mxcBinaryPath` override. MXC's tier
266
+ degradation warnings are logged but do not block activation. To inspect a host,
267
+ run the executor directly:
268
+
269
+ ```powershell
270
+ & node_modules\@microsoft\mxc-sdk\bin\x64\wxc-exec.exe --probe
271
+ ```
272
+
273
+ Use `bin\arm64` on Arm64 hosts.
274
+
275
+ An existing `mxcBinaryPath` override must point to an MXC 0.8.0-compatible
276
+ executor that supports `--probe`. An older executor stops plugin activation;
277
+ the plugin cannot safely infer readiness from a Windows service name or probe a
278
+ different binary. If the override fails, run that exact executable with
279
+ `--probe` to see its error. Update the override to a compatible executor, or
280
+ remove it to use the `@microsoft/mxc-sdk@0.8.0` executor installed with the
281
+ plugin:
282
+
283
+ ```powershell
284
+ openclaw config unset plugins.entries.mxc.config.mxcBinaryPath
285
+ ```
286
+
287
+ Restart the Gateway after changing the override. If the SDK executor also
288
+ fails `--probe`, address the reported host-readiness error before retrying.
262
289
 
263
290
  Host preparation is advisory. If directory listing inside the sandbox fails with
264
291
  `Access is denied`, run this once from an elevated prompt:
package/dist/index.js CHANGED
@@ -192,14 +192,14 @@ var MxcFsBridge = class {
192
192
  }
193
193
  async readFile(params) {
194
194
  const target = this.resolveTarget(params);
195
- return await (await root(target.mount.hostRoot)).readBytes(target.mountRelativePath, {
195
+ return await (await root(target.mount.hostRoot)).readBytes(target.relativePath, {
196
196
  hardlinks: "reject",
197
197
  ...params.maxBytes === void 0 ? {} : { maxBytes: params.maxBytes }
198
198
  });
199
199
  }
200
200
  async readDirectory(params) {
201
201
  const target = this.resolveTarget(params);
202
- return (await (await root(target.mount.hostRoot)).list(target.mountRelativePath, { withFileTypes: true })).map(({ name, isDirectory }) => ({
202
+ return (await (await root(target.mount.hostRoot)).list(target.relativePath, { withFileTypes: true })).map(({ name, isDirectory }) => ({
203
203
  name,
204
204
  isDirectory
205
205
  }));
@@ -208,14 +208,14 @@ var MxcFsBridge = class {
208
208
  const target = this.resolveTarget(params);
209
209
  this.ensureWritable(target, "write files");
210
210
  const buffer = Buffer.isBuffer(params.data) ? params.data : Buffer.from(params.data, params.encoding ?? "utf8");
211
- await (await root(target.mount.hostRoot)).write(target.mountRelativePath, buffer, { mkdir: params.mkdir !== false });
211
+ await (await root(target.mount.hostRoot)).write(target.relativePath, buffer, { mkdir: params.mkdir !== false });
212
212
  }
213
213
  async createFileExclusive(params) {
214
214
  const target = this.resolveTarget(params);
215
215
  this.ensureWritable(target, "create files");
216
216
  const buffer = Buffer.isBuffer(params.data) ? params.data : Buffer.from(params.data, params.encoding ?? "utf8");
217
217
  try {
218
- await (await root(target.mount.hostRoot)).create(target.mountRelativePath, buffer, { mkdir: params.mkdir !== false });
218
+ await (await root(target.mount.hostRoot)).create(target.relativePath, buffer, { mkdir: params.mkdir !== false });
219
219
  return "created";
220
220
  } catch (error) {
221
221
  if (error instanceof FsSafeError && error.code === "already-exists") return "exists";
@@ -225,32 +225,32 @@ var MxcFsBridge = class {
225
225
  async mkdirp(params) {
226
226
  const target = this.resolveTarget(params);
227
227
  this.ensureWritable(target, "create directories");
228
- if (target.mountRelativePath.length === 0) return;
229
- await (await root(target.mount.hostRoot)).mkdir(target.mountRelativePath);
228
+ if (target.relativePath.length === 0) return;
229
+ await (await root(target.mount.hostRoot)).mkdir(target.relativePath);
230
230
  }
231
231
  async remove(params) {
232
232
  const target = this.resolveTarget(params);
233
233
  this.ensureWritable(target, "remove files");
234
234
  await removePathWithinRoot({
235
235
  rootDir: target.mount.hostRoot,
236
- relativePath: target.mountRelativePath,
236
+ relativePath: target.relativePath,
237
237
  recursive: params.recursive,
238
238
  force: params.force ?? false
239
239
  });
240
240
  }
241
241
  async rename(params) {
242
242
  const { from: source, to: target } = this.resolveRenameTargets(params);
243
- if (!isSameMountRoot(source.mount.hostRoot, target.mount.hostRoot)) throw new Error(`Sandbox rename must stay within the same mounted root: ${source.containerPath} -> ${target.containerPath}`);
243
+ if (normalizeMxcPathForComparison(source.mount.hostRoot) !== normalizeMxcPathForComparison(target.mount.hostRoot)) throw new Error(`Sandbox rename must stay within the same mounted root: ${source.containerPath} -> ${target.containerPath}`);
244
244
  const root$1 = await root(source.mount.hostRoot);
245
- const targetParent = resolveRelativeParentPath(target.mountRelativePath);
246
- if (targetParent) await root$1.mkdir(targetParent);
247
- await root$1.move(source.mountRelativePath, target.mountRelativePath, { overwrite: true });
245
+ const targetParent = path.dirname(target.relativePath);
246
+ if (targetParent !== "." && targetParent !== "") await root$1.mkdir(targetParent);
247
+ await root$1.move(source.relativePath, target.relativePath, { overwrite: true });
248
248
  }
249
249
  async stat(params) {
250
250
  const target = this.resolveTarget(params);
251
251
  const root$2 = await root(target.mount.hostRoot);
252
- if (!await root$2.exists(target.mountRelativePath)) return null;
253
- const stats = await root$2.stat(target.mountRelativePath);
252
+ if (!await root$2.exists(target.relativePath)) return null;
253
+ const stats = await root$2.stat(target.relativePath);
254
254
  return {
255
255
  type: stats.isDirectory ? "directory" : stats.isFile ? "file" : "other",
256
256
  size: stats.size,
@@ -261,29 +261,21 @@ var MxcFsBridge = class {
261
261
  const input = params.filePath.trim();
262
262
  const cwd = params.cwd?.trim() ? path.resolve(params.cwd) : this.defaultContainerRoot;
263
263
  const containerPath = path.isAbsolute(input) ? path.resolve(input) : path.resolve(cwd, input);
264
- return this.resolveMountedTarget(containerPath, this.protectedSkillMounts) ?? this.resolveMountedTarget(containerPath, this.workspaceMounts) ?? this.throwSandboxRootEscape(params.filePath);
265
- }
266
- resolveMountedTarget(containerPath, mounts) {
267
- for (const mount of mounts) {
264
+ for (const mount of [...this.protectedSkillMounts, ...this.workspaceMounts]) {
268
265
  if (!isPathInside(mount.containerRoot, containerPath)) continue;
269
266
  const mountRelativePath = path.relative(mount.containerRoot, containerPath);
270
267
  return {
271
268
  hostPath: path.join(mount.hostRoot, mountRelativePath),
272
269
  relativePath: mountRelativePath,
273
270
  containerPath,
274
- mount,
275
- mountRelativePath,
276
- writable: mount.writable
271
+ mount
277
272
  };
278
273
  }
279
- return null;
280
- }
281
- throwSandboxRootEscape(filePath) {
282
274
  const allowedRoots = [...new Set(this.workspaceMounts.map((mount) => mount.containerRoot))].join(", ");
283
- throw new Error(`Path escapes sandbox root (${allowedRoots}; container root ${this.sandbox.containerWorkdir}): ${filePath}. Use a path under ${this.sandbox.containerWorkdir}\\ instead.`);
275
+ throw new Error(`Path escapes sandbox root (${allowedRoots}; container root ${this.sandbox.containerWorkdir}): ${params.filePath}. Use a path under ${this.sandbox.containerWorkdir}\\ instead.`);
284
276
  }
285
277
  ensureWritable(target, action) {
286
- if (!target.writable) throw new Error(`Sandbox path is read-only; cannot ${action}: ${target.containerPath}`);
278
+ if (!target.mount.writable) throw new Error(`Sandbox path is read-only; cannot ${action}: ${target.containerPath}`);
287
279
  }
288
280
  };
289
281
  function resolveWorkspaceMounts(sandbox) {
@@ -321,58 +313,13 @@ function dedupeAndSortMounts(mounts) {
321
313
  const key = `${normalizeMxcPathForComparison(mount.hostRoot)}::${normalizeMxcPathForComparison(mount.containerRoot)}`;
322
314
  if (!deduped.has(key)) deduped.set(key, mount);
323
315
  }
324
- return [...deduped.values()].toSorted((left, right) => {
325
- const lengthDiff = right.containerRoot.length - left.containerRoot.length;
326
- if (lengthDiff !== 0) return lengthDiff;
327
- return right.hostRoot.length - left.hostRoot.length;
328
- });
329
- }
330
- function resolveRelativeParentPath(relativePath) {
331
- const parent = path.dirname(relativePath);
332
- return parent === "." || parent === "" ? null : parent;
333
- }
334
- function isSameMountRoot(first, second) {
335
- return normalizeMxcPathForComparison(first) === normalizeMxcPathForComparison(second);
316
+ return [...deduped.values()].toSorted((left, right) => right.containerRoot.length - left.containerRoot.length || right.hostRoot.length - left.hostRoot.length);
336
317
  }
337
318
  //#endregion
338
319
  //#region extensions/mxc/src/sandbox-baseline.ts
339
- /**
340
- * Deterministic MXC sandbox baseline policy helpers.
341
- *
342
- * These helpers only include policy surfaces the Windows ProcessContainer path
343
- * currently enforces: workspace/read-only/read-write paths and process timeout.
344
- */
345
- const BASELINE_TIMEOUT_SECONDS = 300;
346
- const DEFAULT_SANDBOX_BASELINE = {
347
- filesystem: {
348
- restrictToProjectDir: true,
349
- additionalReadonlyPaths: [],
350
- additionalReadwritePaths: []
351
- },
352
- process: {
353
- timeoutSeconds: BASELINE_TIMEOUT_SECONDS,
354
- timeoutSecondsConfigured: false
355
- }
356
- };
357
320
  function firstNonBlankEnv(...values) {
358
321
  return values.find((value) => value?.trim());
359
322
  }
360
- function resolveSandboxBaseline(input = {}) {
361
- const timeoutSecondsConfigured = input.process?.timeoutSeconds !== void 0;
362
- const timeoutSeconds = input.process?.timeoutSeconds ?? BASELINE_TIMEOUT_SECONDS;
363
- if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 1) throw new RangeError("Sandbox baseline timeoutSeconds must be at least 1.");
364
- return {
365
- filesystem: {
366
- restrictToProjectDir: input.filesystem?.restrictToProjectDir ?? true,
367
- additionalReadonlyPaths: [...input.filesystem?.additionalReadonlyPaths ?? []],
368
- additionalReadwritePaths: [...input.filesystem?.additionalReadwritePaths ?? []]
369
- },
370
- process: {
371
- timeoutSeconds,
372
- timeoutSecondsConfigured
373
- }
374
- };
375
- }
376
323
  function resolveSandboxTempDir(env = {}) {
377
324
  return firstNonBlankEnv(env.TEMP, env.TMP) ?? "C:\\Windows\\Temp";
378
325
  }
@@ -509,7 +456,6 @@ function buildLauncherEnv(hostEnv = process.env) {
509
456
  //#endregion
510
457
  //#region extensions/mxc/src/mxc-container-config.ts
511
458
  const MXC_SCHEMA_VERSION = "0.7.0-alpha";
512
- const PROCESS_CONTAINER_NAME_MAX_LEN = 64;
513
459
  function resolveCurrentBaselineContext(projectDir) {
514
460
  return {
515
461
  projectDir: path.resolve(projectDir),
@@ -559,7 +505,10 @@ function buildMxcContainerConfig(params) {
559
505
  version: MXC_SCHEMA_VERSION,
560
506
  containerId: params.containerId,
561
507
  containment: params.config.containment,
562
- lifecycle: { destroyOnExit: true },
508
+ lifecycle: {
509
+ destroyOnExit: true,
510
+ preservePolicy: false
511
+ },
563
512
  process: {
564
513
  commandLine: buildCommandLine(params.command, params.args ?? []),
565
514
  cwd: params.workdir,
@@ -577,7 +526,6 @@ function buildMxcContainerConfig(params) {
577
526
  enforcementMode: "capabilities"
578
527
  },
579
528
  processContainer: {
580
- name: processContainerName(params.runtimeId),
581
529
  leastPrivilege: true,
582
530
  capabilities: networkAllowed ? ["internetClient"] : [],
583
531
  ui: {
@@ -590,19 +538,20 @@ function buildMxcContainerConfig(params) {
590
538
  };
591
539
  }
592
540
  function buildFilesystemConfig(params) {
593
- const readwritePathSpecs = resolveWorkspaceReadwritePathSpecs(params.workspace);
594
- const readonlyPathSpecs = [
595
- ...resolveWorkspaceReadonlyPathSpecs(params.workspace),
596
- ...resolveBaselineReadonlyPathSpecs(params.baseline, params.context),
597
- ...resolveProtectedSkillPolicyPathSpecs(params.workspace)
598
- ];
599
- if (params.baseline.filesystem.restrictToProjectDir) {
600
- const projectDirPath = params.context.projectDir;
601
- if (params.workspace.workspaceAccess === "rw") readwritePathSpecs.push(requiredFilesystemPath(projectDirPath));
602
- else readonlyPathSpecs.push(requiredFilesystemPath(projectDirPath));
603
- readwritePathSpecs.push(requiredFilesystemPath(path.resolve(params.sandboxTempDir)));
604
- readwritePathSpecs.push(...params.baseline.configuredPaths.readwritePaths.map(createConfiguredFilesystemPath));
541
+ const readwritePathSpecs = [];
542
+ const readonlyPathSpecs = [];
543
+ const workspace = params.workspace;
544
+ if (workspace.workspaceAccess === "rw") readwritePathSpecs.push(requiredFilesystemPath(workspace.activeWorkspaceDir));
545
+ else {
546
+ readonlyPathSpecs.push(requiredFilesystemPath(workspace.workspaceDir));
547
+ if (workspace.workspaceAccess === "ro" && normalizeMxcPathForComparison(workspace.agentWorkspaceDir) !== normalizeMxcPathForComparison(workspace.workspaceDir)) readonlyPathSpecs.push(requiredFilesystemPath(workspace.agentWorkspaceDir));
605
548
  }
549
+ readonlyPathSpecs.push(...resolveBaselineReadonlyPaths(params.context.hostEnv).map((candidatePath) => optionalFilesystemPath(path.resolve(candidatePath))), ...params.baseline.configuredPaths.readonlyPaths.map(createConfiguredFilesystemPath), ...resolveMxcProtectedSkillPolicyPaths(workspace).map(optionalFilesystemPath));
550
+ const projectDirPath = params.context.projectDir;
551
+ if (params.workspace.workspaceAccess === "rw") readwritePathSpecs.push(requiredFilesystemPath(projectDirPath));
552
+ else readonlyPathSpecs.push(requiredFilesystemPath(projectDirPath));
553
+ readwritePathSpecs.push(requiredFilesystemPath(path.resolve(params.sandboxTempDir)));
554
+ readwritePathSpecs.push(...params.baseline.configuredPaths.readwritePaths.map(createConfiguredFilesystemPath));
606
555
  const protectedSkillPolicyPaths = resolveMxcProtectedSkillPolicyPaths(params.workspace);
607
556
  assertNoMxcReadwriteReadonlyOverlap({
608
557
  readwritePaths: resolveExistingFilesystemPaths(readwritePathSpecs, "readwrite"),
@@ -617,23 +566,9 @@ function buildFilesystemConfig(params) {
617
566
  return {
618
567
  readonlyPaths,
619
568
  deniedPaths: void 0,
620
- readwritePaths,
621
- clearPolicyOnExit: true
569
+ readwritePaths
622
570
  };
623
571
  }
624
- function resolveWorkspaceReadwritePathSpecs(workspace) {
625
- if (workspace.workspaceAccess !== "rw") return [];
626
- return [requiredFilesystemPath(workspace.activeWorkspaceDir)];
627
- }
628
- function resolveWorkspaceReadonlyPathSpecs(workspace) {
629
- if (workspace.workspaceAccess === "rw") return [];
630
- const readonlyPathSpecs = [requiredFilesystemPath(workspace.workspaceDir)];
631
- if (workspace.workspaceAccess === "ro" && normalizeMxcPathForComparison(workspace.agentWorkspaceDir) !== normalizeMxcPathForComparison(workspace.workspaceDir)) readonlyPathSpecs.push(requiredFilesystemPath(workspace.agentWorkspaceDir));
632
- return readonlyPathSpecs;
633
- }
634
- function resolveBaselineReadonlyPathSpecs(baseline, context) {
635
- return [...resolveBaselineReadonlyPaths(context.hostEnv).map((candidatePath) => optionalFilesystemPath(path.resolve(candidatePath))), ...baseline.configuredPaths.readonlyPaths.map(createConfiguredFilesystemPath)];
636
- }
637
572
  function resolveMxcProtectedSkillPolicyPaths(context) {
638
573
  const deduped = /* @__PURE__ */ new Map();
639
574
  for (const mount of resolveMxcReadOnlySkillMounts(context)) {
@@ -644,9 +579,6 @@ function resolveMxcProtectedSkillPolicyPaths(context) {
644
579
  }
645
580
  return [...deduped.values()];
646
581
  }
647
- function resolveProtectedSkillPolicyPathSpecs(context) {
648
- return resolveMxcProtectedSkillPolicyPaths(context).map((candidatePath) => optionalFilesystemPath(candidatePath));
649
- }
650
582
  function resolveExistingFilesystemPaths(pathSpecs, accessLabel) {
651
583
  const deduped = /* @__PURE__ */ new Map();
652
584
  for (const pathSpec of pathSpecs) {
@@ -698,11 +630,6 @@ function buildMissingFilesystemPathMessage(pathValue, accessLabel, sources) {
698
630
  if (sourceLabel) return `MXC sandbox ${accessLabel} path ${pathValue} configured by ${sourceLabel} is missing on the host. Recreate the path or update the policy file before launching the sandbox.`;
699
631
  return `MXC sandbox ${accessLabel} path ${pathValue} does not exist on the host.`;
700
632
  }
701
- function processContainerName(runtimeId) {
702
- if (runtimeId.length <= PROCESS_CONTAINER_NAME_MAX_LEN) return runtimeId;
703
- const hash = createHash("sha256").update(runtimeId).digest("hex").slice(0, 8);
704
- return `${runtimeId.slice(0, PROCESS_CONTAINER_NAME_MAX_LEN - hash.length - 1)}-${hash}`;
705
- }
706
633
  function resolveProcessTimeoutSeconds(config, baseline) {
707
634
  if (config.timeoutSecondsConfigured === true) return Math.min(config.timeoutSeconds, baseline.process.timeoutSeconds);
708
635
  return baseline.process.timeoutSeconds;
@@ -767,17 +694,7 @@ const SandboxPolicyLayerSchema = z.object({
767
694
  process: processPolicySchema.optional()
768
695
  }).strict();
769
696
  function loadSandboxBaselinePolicy(options = {}) {
770
- const layers = (options.policyPaths ?? []).map(readSandboxPolicyFile);
771
- const merged = mergeSandboxPolicyLayers(layers);
772
- const resolved = resolveSandboxBaseline(merged);
773
- return {
774
- ...resolved,
775
- process: {
776
- ...resolved.process,
777
- timeoutSecondsConfigured: layers.some((policy) => policy.process?.timeoutSeconds !== void 0)
778
- },
779
- configuredPaths: merged.configuredPaths
780
- };
697
+ return mergeSandboxPolicyLayers((options.policyPaths ?? []).map(readSandboxPolicyFile));
781
698
  }
782
699
  function readSandboxPolicyFile(policyPath) {
783
700
  try {
@@ -793,7 +710,7 @@ function parseSandboxPolicyLayer(value, sourceLabel) {
793
710
  const readonlyPaths = normalizeConfiguredPaths(filesystem?.additionalReadonlyPaths, sourceLabel, "filesystem.additionalReadonlyPaths");
794
711
  const readwritePaths = normalizeConfiguredPaths(filesystem?.additionalReadwritePaths, sourceLabel, "filesystem.additionalReadwritePaths");
795
712
  return {
796
- ...parsed.data,
713
+ process: parsed.data.process,
797
714
  configuredPaths: {
798
715
  readonlyPaths,
799
716
  readwritePaths
@@ -801,7 +718,7 @@ function parseSandboxPolicyLayer(value, sourceLabel) {
801
718
  };
802
719
  }
803
720
  function mergeSandboxPolicyLayers(layers) {
804
- const timeoutCandidates = [DEFAULT_SANDBOX_BASELINE.process.timeoutSeconds];
721
+ const timeoutCandidates = [300];
805
722
  const configuredPathMaps = {
806
723
  readonlyPaths: /* @__PURE__ */ new Map(),
807
724
  readwritePaths: /* @__PURE__ */ new Map()
@@ -813,11 +730,6 @@ function mergeSandboxPolicyLayers(layers) {
813
730
  if (timeoutSeconds !== void 0) timeoutCandidates.push(timeoutSeconds);
814
731
  }
815
732
  return {
816
- filesystem: {
817
- restrictToProjectDir: DEFAULT_SANDBOX_BASELINE.filesystem.restrictToProjectDir,
818
- additionalReadonlyPaths: [...configuredPathMaps.readonlyPaths.values()].map((entry) => entry.path),
819
- additionalReadwritePaths: [...configuredPathMaps.readwritePaths.values()].map((entry) => entry.path)
820
- },
821
733
  process: { timeoutSeconds: Math.min(...timeoutCandidates) },
822
734
  configuredPaths: {
823
735
  readonlyPaths: [...configuredPathMaps.readonlyPaths.values()],
@@ -884,7 +796,6 @@ function formatSandboxPolicyIssue(sourceLabel, issue) {
884
796
  const fieldLabel = `${sourceLabel}${formatIssuePath(issue.path)}`;
885
797
  if (issue.code === "unrecognized_keys" && issue.keys.length > 0) return `Sandbox policy field ${fieldLabel}.${issue.keys[0]} is not supported.`;
886
798
  if (issue.code === "invalid_type" && issue.path.length === 1) return `Sandbox policy section ${fieldLabel} must be a JSON object.`;
887
- if (issue.code === "invalid_type") return `Sandbox policy field ${fieldLabel} ${issue.message}.`;
888
799
  if (issue.code === "too_small") return `Sandbox policy field ${fieldLabel} must be a positive number.`;
889
800
  return `Sandbox policy field ${fieldLabel} ${issue.message}.`;
890
801
  }
@@ -904,31 +815,11 @@ function isNodeError(err) {
904
815
  }
905
816
  //#endregion
906
817
  //#region extensions/mxc/src/mxc-backend.ts
907
- const CONTAINER_ID_MAX_LEN = 80;
818
+ const CONTAINER_ID_MAX_LEN = 64;
908
819
  function uniqueContainerId(runtimeId) {
909
820
  const suffix = randomBytes(4).toString("hex");
910
821
  return `${runtimeId.length + suffix.length + 1 > CONTAINER_ID_MAX_LEN ? runtimeId.slice(0, CONTAINER_ID_MAX_LEN - suffix.length - 1) : runtimeId}-${suffix}`;
911
822
  }
912
- function createLauncherPayloadFile(payloadJson) {
913
- const payloadDir = mkdtempSync(path.join(resolvePreferredOpenClawTmpDir(), "openclaw-mxc-payload-"));
914
- const payloadFile = path.join(payloadDir, "payload.json");
915
- try {
916
- writeFileSync(payloadFile, payloadJson, {
917
- flag: "wx",
918
- mode: 384
919
- });
920
- } catch (err) {
921
- rmSync(payloadDir, {
922
- force: true,
923
- recursive: true
924
- });
925
- throw err;
926
- }
927
- return {
928
- payloadDir,
929
- payloadFile
930
- };
931
- }
932
823
  function cleanupLauncherPayloadFile(token) {
933
824
  if (token && typeof token === "object" && "payloadDir" in token && typeof token.payloadDir === "string") {
934
825
  rmSync(token.payloadDir, {
@@ -969,29 +860,34 @@ function isMissingPathError(err) {
969
860
  const code = extractErrorCode(err);
970
861
  return code === "ENOENT" || code === "ENOTDIR";
971
862
  }
972
- function buildMxcLauncherOptions(config, usePty) {
973
- const options = {
974
- debug: config.debug ?? false,
975
- executablePath: resolveMxcBinaryPath(config.mxcBinaryPath)
976
- };
977
- if (!usePty) options.usePty = false;
978
- return options;
979
- }
980
863
  function createMxcLauncherPayload(config, payload, usePty, sandboxTempDir) {
981
- const token = createLauncherPayloadFile(JSON.stringify({
864
+ const payloadJson = JSON.stringify({
982
865
  config: payload,
983
- options: buildMxcLauncherOptions(config, usePty)
984
- }));
985
- token.sandboxTempDir = sandboxTempDir;
986
- return token;
987
- }
988
- function buildMxcLauncherArgv(payloadFile) {
989
- return [
990
- process.execPath,
991
- resolveMxcLauncherPath(),
992
- "--payload-file",
993
- payloadFile
994
- ];
866
+ options: {
867
+ debug: config.debug ?? false,
868
+ executablePath: resolveMxcBinaryPath(config.mxcBinaryPath),
869
+ ...!usePty ? { usePty: false } : {}
870
+ }
871
+ });
872
+ const payloadDir = mkdtempSync(path.join(resolvePreferredOpenClawTmpDir(), "openclaw-mxc-payload-"));
873
+ const payloadFile = path.join(payloadDir, "payload.json");
874
+ try {
875
+ writeFileSync(payloadFile, payloadJson, {
876
+ flag: "wx",
877
+ mode: 384
878
+ });
879
+ } catch (err) {
880
+ rmSync(payloadDir, {
881
+ force: true,
882
+ recursive: true
883
+ });
884
+ throw err;
885
+ }
886
+ return {
887
+ payloadDir,
888
+ payloadFile,
889
+ sandboxTempDir
890
+ };
995
891
  }
996
892
  /**
997
893
  * Creates a SandboxBackendHandle for a specific session.
@@ -1028,7 +924,6 @@ function createMxcSandboxBackendHandle(params) {
1028
924
  config: params.config,
1029
925
  baseline,
1030
926
  baselineContext,
1031
- runtimeId: params.runtimeId,
1032
927
  containerId: uniqueContainerId(params.runtimeId),
1033
928
  command,
1034
929
  sandboxTempDir,
@@ -1038,7 +933,12 @@ function createMxcSandboxBackendHandle(params) {
1038
933
  });
1039
934
  const payloadFile = createMxcLauncherPayload(params.config, payload, usePty, sandboxTempDir);
1040
935
  return {
1041
- argv: buildMxcLauncherArgv(payloadFile.payloadFile),
936
+ argv: [
937
+ process.execPath,
938
+ resolveMxcLauncherPath(),
939
+ "--payload-file",
940
+ payloadFile.payloadFile
941
+ ],
1042
942
  env: buildLauncherEnv(),
1043
943
  stdinMode: usePty ? "pipe-open" : "pipe-closed",
1044
944
  finalizeToken: payloadFile
@@ -1078,12 +978,11 @@ function createMxcSandboxBackendHandle(params) {
1078
978
  script: cmdParams.script,
1079
979
  tempDir: sandboxTempDir
1080
980
  });
1081
- const execInput = cmdParams.stdin === void 0 ? Buffer.alloc(0) : toBuffer(cmdParams.stdin);
981
+ const execInput = Buffer.isBuffer(cmdParams.stdin) ? cmdParams.stdin : Buffer.from(cmdParams.stdin ?? "", "utf-8");
1082
982
  const payloadFile = createMxcLauncherPayload(restrictiveConfig, buildMxcContainerConfig({
1083
983
  config: restrictiveConfig,
1084
984
  baseline,
1085
985
  baselineContext,
1086
- runtimeId: params.runtimeId,
1087
986
  containerId: uniqueContainerId(params.runtimeId),
1088
987
  command: commandBridge.command,
1089
988
  args: cmdParams.args,
@@ -1092,7 +991,12 @@ function createMxcSandboxBackendHandle(params) {
1092
991
  workspace,
1093
992
  env: {}
1094
993
  }), false, sandboxTempDir);
1095
- const argv = buildMxcLauncherArgv(payloadFile.payloadFile);
994
+ const argv = [
995
+ process.execPath,
996
+ resolveMxcLauncherPath(),
997
+ "--payload-file",
998
+ payloadFile.payloadFile
999
+ ];
1096
1000
  try {
1097
1001
  const result = await runCommandBuffered(argv, {
1098
1002
  baseEnv: buildLauncherEnv(),
@@ -1133,10 +1037,6 @@ function createMxcSandboxBackendHandle(params) {
1133
1037
  }
1134
1038
  };
1135
1039
  }
1136
- function toBuffer(value) {
1137
- if (Buffer.isBuffer(value)) return value;
1138
- return Buffer.from(value, "utf-8");
1139
- }
1140
1040
  /** Manager for `openclaw sandbox list` and `openclaw sandbox remove`. */
1141
1041
  const mxcSandboxBackendManager = {
1142
1042
  async describeRuntime() {
@@ -1172,29 +1072,52 @@ function createMxcSandboxBackendFactory(config) {
1172
1072
  }
1173
1073
  //#endregion
1174
1074
  //#region extensions/mxc/src/readiness.ts
1175
- const DEFAULT_DEPS = { execFileSync };
1075
+ const MxcProbeOutputSchema = z.object({
1076
+ tier: z.enum([
1077
+ "base-container",
1078
+ "appcontainer-bfs",
1079
+ "appcontainer-dacl"
1080
+ ]).optional(),
1081
+ warnings: z.array(z.string()).default([]),
1082
+ error: z.string().optional()
1083
+ });
1176
1084
  function resolveWindowsSystemExecutable(name) {
1177
1085
  const systemRoot = process.env.SystemRoot || process.env.WINDIR;
1178
1086
  return path.win32.join(systemRoot || "C:\\Windows", "System32", name);
1179
1087
  }
1180
- function assertWindowsIsoEnvBrokerInstalled(deps) {
1088
+ function probeMxcIsolationTier(executablePath) {
1089
+ const notReady = (reason, cause) => new Error(`[mxc] MXC Windows ProcessContainer sandbox is not ready: ${reason}. Run "${executablePath}" --probe for host details. The selected executor must be compatible with MXC 0.8.0 and support --probe. If mxcBinaryPath points to an older executor, update it or unset plugins.entries.mxc.config.mxcBinaryPath and restart the Gateway to use the bundled SDK executor.`, cause === void 0 ? void 0 : { cause });
1090
+ let output;
1181
1091
  try {
1182
- deps.execFileSync(resolveWindowsSystemExecutable("sc.exe"), ["query", "IsoEnvBroker"], {
1092
+ output = execFileSync(executablePath, ["--probe"], {
1183
1093
  encoding: "utf-8",
1184
1094
  stdio: "pipe",
1185
- timeout: 5e3,
1095
+ timeout: 15e3,
1186
1096
  windowsHide: true
1187
1097
  });
1188
1098
  } catch (error) {
1189
- const detail = error instanceof Error && error.message ? `: ${error.message.trim()}` : "";
1190
- throw new Error(`[mxc] MXC Windows ProcessContainer sandbox is not ready: IsoEnvBroker service is not installed${detail}. Install the IsoEnvBroker service before enabling MXC sandbox execution.`, { cause: error });
1099
+ throw notReady(`the MXC host probe failed${error instanceof Error && error.message ? `: ${error.message.trim()}` : ""}`, error);
1191
1100
  }
1101
+ let probe;
1102
+ try {
1103
+ probe = JSON.parse(output);
1104
+ } catch (error) {
1105
+ throw notReady("the MXC host probe did not return JSON", error);
1106
+ }
1107
+ const parsed = MxcProbeOutputSchema.safeParse(probe);
1108
+ if (!parsed.success) throw notReady("the MXC host probe returned an unexpected result", parsed.error);
1109
+ const { tier, warnings, error } = parsed.data;
1110
+ if (!tier) throw notReady(`MXC cannot select an isolation tier on this host (${error || "the probe reported no isolation tier"})`);
1111
+ return {
1112
+ tier,
1113
+ warnings
1114
+ };
1192
1115
  }
1193
- function isSystemDrivePrepared(deps) {
1116
+ function isSystemDrivePrepared() {
1194
1117
  const systemDrive = process.env.SystemDrive || "C:";
1195
1118
  let output;
1196
1119
  try {
1197
- output = deps.execFileSync(resolveWindowsSystemExecutable("icacls.exe"), [`${systemDrive}\\`], {
1120
+ output = execFileSync(resolveWindowsSystemExecutable("icacls.exe"), [`${systemDrive}\\`], {
1198
1121
  encoding: "utf-8",
1199
1122
  stdio: "pipe",
1200
1123
  timeout: 5e3,
@@ -1212,19 +1135,19 @@ function systemDrivePrepWarning(systemDrive) {
1212
1135
  * Emits an advisory warning when the system drive is not prepared for
1213
1136
  * AppContainer directory access. Non-fatal: the sandbox still activates.
1214
1137
  */
1215
- function warnMxcHostPrepIfNeeded(params = {}) {
1216
- if ((params.platform ?? process.platform) !== "win32") return;
1217
- if (!isSystemDrivePrepared({
1218
- ...DEFAULT_DEPS,
1219
- ...params.deps
1220
- })) (params.warn ?? ((message) => console.warn(message)))(systemDrivePrepWarning(process.env.SystemDrive || "C:"));
1221
- }
1222
- function assertMxcReadiness(params = {}) {
1223
- if ((params.platform ?? process.platform) !== "win32") return;
1224
- assertWindowsIsoEnvBrokerInstalled({
1225
- ...DEFAULT_DEPS,
1226
- ...params.deps
1227
- });
1138
+ function warnMxcHostPrepIfNeeded() {
1139
+ if (process.platform !== "win32") return;
1140
+ if (!isSystemDrivePrepared()) console.warn(systemDrivePrepWarning(process.env.SystemDrive || "C:"));
1141
+ }
1142
+ /**
1143
+ * Fails plugin activation unless MXC's host probe selects an isolation tier for
1144
+ * `executablePath`. Degradation warnings from the probe are reported but do not
1145
+ * block activation.
1146
+ */
1147
+ function assertMxcReadiness(params) {
1148
+ if (process.platform !== "win32") return;
1149
+ const probe = probeMxcIsolationTier(params.executablePath);
1150
+ if (probe.warnings.length > 0) console.warn(`[mxc] MXC sandbox is using the ${probe.tier} isolation tier: ${probe.warnings.join("; ")}`);
1228
1151
  }
1229
1152
  //#endregion
1230
1153
  //#region extensions/mxc/src/plugin.ts
@@ -1235,13 +1158,14 @@ function registerMxcPlugin(api) {
1235
1158
  console.warn(`[mxc] Sandbox backend is Windows-only and not available on ${process.platform}. Plugin will be dormant.`);
1236
1159
  return;
1237
1160
  }
1161
+ let mxcBinaryPath;
1238
1162
  try {
1239
- resolveMxcBinaryPath(config.mxcBinaryPath);
1163
+ mxcBinaryPath = resolveMxcBinaryPath(config.mxcBinaryPath);
1240
1164
  } catch (err) {
1241
1165
  const reason = err instanceof Error ? err.message : String(err);
1242
1166
  throw new Error(`[mxc] MXC sandbox backend cannot load: ${reason}. Install @microsoft/mxc-sdk or set mxcBinaryPath.`, { cause: err });
1243
1167
  }
1244
- assertMxcReadiness();
1168
+ assertMxcReadiness({ executablePath: mxcBinaryPath });
1245
1169
  warnMxcHostPrepIfNeeded();
1246
1170
  const unregister = registerSandboxBackend("mxc", {
1247
1171
  factory: createMxcSandboxBackendFactory(config),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/mxc-sandbox",
3
- "version": "2026.9.8",
3
+ "version": "2026.10.1-beta.2",
4
4
  "description": "OpenClaw MXC sandbox execution plugin for MXC-capable hosts",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,10 +22,10 @@
22
22
  "minHostVersion": ">=2026.6.11"
23
23
  },
24
24
  "compat": {
25
- "pluginApi": ">=2026.9.8"
25
+ "pluginApi": ">=2026.10.1-beta.2"
26
26
  },
27
27
  "build": {
28
- "openclawVersion": "2026.9.8",
28
+ "openclawVersion": "2026.10.1-beta.2",
29
29
  "bundledDist": false,
30
30
  "staticAssets": [
31
31
  {
@@ -50,7 +50,7 @@
50
50
  "assets/activity.svg"
51
51
  ],
52
52
  "peerDependencies": {
53
- "openclaw": ">=2026.9.8"
53
+ "openclaw": ">=2026.10.1-beta.2"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "openclaw": {