@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.
- package/README.md +32 -5
- package/dist/index.js +126 -202
- 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
|
|
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.
|
|
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
|
-
|
|
261
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
229
|
-
await (await root(target.mount.hostRoot)).mkdir(target.
|
|
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.
|
|
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 (
|
|
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 =
|
|
246
|
-
if (targetParent) await root$1.mkdir(targetParent);
|
|
247
|
-
await root$1.move(source.
|
|
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.
|
|
253
|
-
const stats = await root$2.stat(target.
|
|
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
|
-
|
|
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: {
|
|
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 =
|
|
594
|
-
const readonlyPathSpecs = [
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = [
|
|
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 =
|
|
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
|
|
864
|
+
const payloadJson = JSON.stringify({
|
|
982
865
|
config: payload,
|
|
983
|
-
options:
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
1092
|
+
output = execFileSync(executablePath, ["--probe"], {
|
|
1183
1093
|
encoding: "utf-8",
|
|
1184
1094
|
stdio: "pipe",
|
|
1185
|
-
timeout:
|
|
1095
|
+
timeout: 15e3,
|
|
1186
1096
|
windowsHide: true
|
|
1187
1097
|
});
|
|
1188
1098
|
} catch (error) {
|
|
1189
|
-
|
|
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(
|
|
1116
|
+
function isSystemDrivePrepared() {
|
|
1194
1117
|
const systemDrive = process.env.SystemDrive || "C:";
|
|
1195
1118
|
let output;
|
|
1196
1119
|
try {
|
|
1197
|
-
output =
|
|
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(
|
|
1216
|
-
if (
|
|
1217
|
-
if (!isSystemDrivePrepared(
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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.
|
|
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.
|
|
25
|
+
"pluginApi": ">=2026.10.1-beta.2"
|
|
26
26
|
},
|
|
27
27
|
"build": {
|
|
28
|
-
"openclawVersion": "2026.
|
|
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.
|
|
53
|
+
"openclaw": ">=2026.10.1-beta.2"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"openclaw": {
|