mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-19 19:01:40 +00:00
* 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>
227 lines
6.9 KiB
TypeScript
227 lines
6.9 KiB
TypeScript
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, test } from "vitest";
|
|
import { loadSandboxBaselinePolicy } from "../src/sandbox-policy-loader.js";
|
|
|
|
const describeOnWindows = describe.runIf(process.platform === "win32");
|
|
|
|
const testDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const dir of testDirs.splice(0)) {
|
|
rmSync(dir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
function makeTestDir(): string {
|
|
const dir = mkdtempSync(join(tmpdir(), "openclaw-mxc-policy-"));
|
|
testDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
function makeExistingDir(rootDir: string, name: string): string {
|
|
const dir = join(rootDir, name);
|
|
mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
function writePolicy(path: string, policy: unknown): void {
|
|
writeFileSync(path, `${JSON.stringify(policy)}\n`, "utf-8");
|
|
}
|
|
|
|
function expectPolicyFileFailure(policyPath: string, action: () => unknown, detail: string): void {
|
|
try {
|
|
action();
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(Error);
|
|
expect((err as Error).message).toContain(policyPath);
|
|
expect((err as Error).message).toContain(detail);
|
|
return;
|
|
}
|
|
throw new Error("Expected policy loader failure.");
|
|
}
|
|
|
|
describeOnWindows("loadSandboxBaselinePolicy", () => {
|
|
test("resolves no configured policy files with the default baseline", () => {
|
|
const policy = loadSandboxBaselinePolicy();
|
|
|
|
expect(policy.filesystem.restrictToProjectDir).toBe(true);
|
|
expect(policy.filesystem.additionalReadonlyPaths).toEqual([]);
|
|
expect(policy.filesystem.additionalReadwritePaths).toEqual([]);
|
|
expect(policy.process.timeoutSeconds).toBe(300);
|
|
expect(policy.process.timeoutSecondsConfigured).toBe(false);
|
|
expect(policy.configuredPaths.readonlyPaths).toEqual([]);
|
|
expect(policy.configuredPaths.readwritePaths).toEqual([]);
|
|
});
|
|
|
|
test("fails closed when a configured policy file is missing", () => {
|
|
const missingPolicyPath = join(makeTestDir(), "missing-policy.json");
|
|
|
|
expect(() => loadSandboxBaselinePolicy({ policyPaths: [missingPolicyPath] })).toThrow(
|
|
/does not exist/u,
|
|
);
|
|
});
|
|
|
|
test("layers configured policy files in deterministic array order", () => {
|
|
const dir = makeTestDir();
|
|
const firstPolicyPath = join(dir, "first-policy.json");
|
|
const secondPolicyPath = join(dir, "second-policy.json");
|
|
const firstReadonlyPath = makeExistingDir(dir, "readonly-first");
|
|
const secondReadonlyPath = makeExistingDir(dir, "readonly-second");
|
|
const firstReadwritePath = makeExistingDir(dir, "readwrite-first");
|
|
const sharedReadwritePath = makeExistingDir(dir, "readwrite-shared");
|
|
const secondReadwritePath = makeExistingDir(dir, "readwrite-second");
|
|
|
|
writePolicy(firstPolicyPath, {
|
|
filesystem: {
|
|
additionalReadonlyPaths: [` ${firstReadonlyPath} `],
|
|
additionalReadwritePaths: [firstReadwritePath, sharedReadwritePath],
|
|
},
|
|
process: {
|
|
timeoutSeconds: 90,
|
|
},
|
|
});
|
|
writePolicy(secondPolicyPath, {
|
|
filesystem: {
|
|
additionalReadonlyPaths: [secondReadonlyPath],
|
|
additionalReadwritePaths: [secondReadwritePath, sharedReadwritePath],
|
|
},
|
|
process: {
|
|
timeoutSeconds: 120,
|
|
},
|
|
});
|
|
|
|
const policy = loadSandboxBaselinePolicy({ policyPaths: [firstPolicyPath, secondPolicyPath] });
|
|
|
|
expect(policy.filesystem.additionalReadonlyPaths).toEqual([
|
|
firstReadonlyPath,
|
|
secondReadonlyPath,
|
|
]);
|
|
expect(policy.filesystem.additionalReadwritePaths).toEqual([
|
|
firstReadwritePath,
|
|
sharedReadwritePath,
|
|
secondReadwritePath,
|
|
]);
|
|
expect(policy.configuredPaths.readwritePaths).toEqual([
|
|
{
|
|
path: firstReadwritePath,
|
|
sources: [`${firstPolicyPath}.filesystem.additionalReadwritePaths[0]`],
|
|
},
|
|
{
|
|
path: sharedReadwritePath,
|
|
sources: [
|
|
`${firstPolicyPath}.filesystem.additionalReadwritePaths[1]`,
|
|
`${secondPolicyPath}.filesystem.additionalReadwritePaths[1]`,
|
|
],
|
|
},
|
|
{
|
|
path: secondReadwritePath,
|
|
sources: [`${secondPolicyPath}.filesystem.additionalReadwritePaths[0]`],
|
|
},
|
|
]);
|
|
expect(policy.process.timeoutSeconds).toBe(90);
|
|
expect(policy.process.timeoutSecondsConfigured).toBe(true);
|
|
});
|
|
|
|
test("hardening booleans can only remain restrictive", () => {
|
|
const dir = makeTestDir();
|
|
const policyPath = join(dir, "policy.json");
|
|
writePolicy(policyPath, {
|
|
filesystem: {
|
|
restrictToProjectDir: false,
|
|
},
|
|
});
|
|
|
|
expect(() => loadSandboxBaselinePolicy({ policyPaths: [policyPath] })).toThrow(
|
|
/restrictToProjectDir/u,
|
|
);
|
|
});
|
|
});
|
|
|
|
describeOnWindows("loadSandboxBaselinePolicy validation", () => {
|
|
test("fails closed for blank read-only paths", () => {
|
|
const dir = makeTestDir();
|
|
const policyPath = join(dir, "blank-path.json");
|
|
writePolicy(policyPath, {
|
|
filesystem: {
|
|
additionalReadonlyPaths: [" "],
|
|
},
|
|
});
|
|
|
|
expectPolicyFileFailure(
|
|
policyPath,
|
|
() => loadSandboxBaselinePolicy({ policyPaths: [policyPath] }),
|
|
"must not be blank",
|
|
);
|
|
});
|
|
|
|
test("fails closed for relative read-write paths", () => {
|
|
const dir = makeTestDir();
|
|
const policyPath = join(dir, "relative-path.json");
|
|
writePolicy(policyPath, {
|
|
filesystem: {
|
|
additionalReadwritePaths: ["relative\\path"],
|
|
},
|
|
});
|
|
|
|
expectPolicyFileFailure(
|
|
policyPath,
|
|
() => loadSandboxBaselinePolicy({ policyPaths: [policyPath] }),
|
|
"absolute Windows path",
|
|
);
|
|
});
|
|
|
|
test("fails closed for nonexistent read-only paths", () => {
|
|
const dir = makeTestDir();
|
|
const policyPath = join(dir, "missing-path.json");
|
|
writePolicy(policyPath, {
|
|
filesystem: {
|
|
additionalReadonlyPaths: [join(dir, "missing-readonly")],
|
|
},
|
|
});
|
|
|
|
expectPolicyFileFailure(
|
|
policyPath,
|
|
() => loadSandboxBaselinePolicy({ policyPaths: [policyPath] }),
|
|
"does not exist on the host",
|
|
);
|
|
});
|
|
|
|
test("fails closed with path-inclusive errors for malformed existing files", () => {
|
|
const dir = makeTestDir();
|
|
const cases: ReadonlyArray<{
|
|
name: string;
|
|
content: string;
|
|
detail: string;
|
|
}> = [
|
|
{
|
|
name: "invalid-json.json",
|
|
content: "{",
|
|
detail: "Failed to load sandbox policy file at",
|
|
},
|
|
{
|
|
name: "array.json",
|
|
content: "[]",
|
|
detail: "must be a JSON object",
|
|
},
|
|
{
|
|
name: "unknown-key.json",
|
|
content: JSON.stringify({ network: { additionalDeniedHosts: ["metadata"] } }),
|
|
detail: "is not supported",
|
|
},
|
|
];
|
|
|
|
for (const item of cases) {
|
|
const policyPath = join(dir, item.name);
|
|
writeFileSync(policyPath, item.content, "utf-8");
|
|
|
|
expectPolicyFileFailure(
|
|
policyPath,
|
|
() => loadSandboxBaselinePolicy({ policyPaths: [policyPath] }),
|
|
item.detail,
|
|
);
|
|
}
|
|
});
|
|
});
|