OpenShell: constrain mirror sync roots (#58515)

* fix(openshell): constrain mirror sync roots

* fix(openshell): restore config test types

* fix(openshell): simplify managed root sync
This commit is contained in:
Agustin Rivera
2026-04-02 06:21:30 -07:00
committed by GitHub
parent 3e4de956c0
commit b21c9840c2
3 changed files with 103 additions and 52 deletions

View File

@@ -0,0 +1,72 @@
import fsSync from "node:fs";
import { describe, expect, it } from "vitest";
import { createOpenShellPluginConfigSchema, resolveOpenShellPluginConfig } from "./config.js";
describe("openshell plugin config", () => {
it("applies defaults", () => {
expect(resolveOpenShellPluginConfig(undefined)).toEqual({
mode: "mirror",
command: "openshell",
gateway: undefined,
gatewayEndpoint: undefined,
from: "openclaw",
policy: undefined,
providers: [],
gpu: false,
autoProviders: true,
remoteWorkspaceDir: "/sandbox",
remoteAgentWorkspaceDir: "/agent",
timeoutMs: 120_000,
});
});
it("accepts remote mode", () => {
expect(resolveOpenShellPluginConfig({ mode: "remote" }).mode).toBe("remote");
});
it("rejects relative remote paths", () => {
expect(() =>
resolveOpenShellPluginConfig({
remoteWorkspaceDir: "sandbox",
}),
).toThrow("OpenShell remoteWorkspaceDir must be absolute");
});
it("rejects remote paths outside managed sandbox roots", () => {
expect(() =>
resolveOpenShellPluginConfig({
remoteWorkspaceDir: "/tmp/victim",
}),
).toThrow("OpenShell remoteWorkspaceDir must stay under /sandbox or /agent");
});
it("normalizes managed sandbox subpaths", () => {
expect(
resolveOpenShellPluginConfig({
remoteWorkspaceDir: "/sandbox/../sandbox/project",
remoteAgentWorkspaceDir: "/agent/./session",
}),
).toEqual(
expect.objectContaining({
remoteWorkspaceDir: "/sandbox/project",
remoteAgentWorkspaceDir: "/agent/session",
}),
);
});
it("rejects unknown mode", () => {
expect(() =>
resolveOpenShellPluginConfig({
mode: "bogus",
}),
).toThrow("mode must be one of mirror, remote");
});
it("keeps the runtime json schema in sync with the manifest config schema", () => {
const manifest = JSON.parse(
fsSync.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"),
) as { configSchema?: unknown };
expect(createOpenShellPluginConfigSchema().jsonSchema).toEqual(manifest.configSchema);
});
});

View File

@@ -38,6 +38,10 @@ const DEFAULT_SOURCE = "openclaw";
const DEFAULT_REMOTE_WORKSPACE_DIR = "/sandbox";
const DEFAULT_REMOTE_AGENT_WORKSPACE_DIR = "/agent";
const DEFAULT_TIMEOUT_MS = 120_000;
const OPEN_SHELL_MANAGED_REMOTE_ROOTS = [
DEFAULT_REMOTE_WORKSPACE_DIR,
DEFAULT_REMOTE_AGENT_WORKSPACE_DIR,
] as const;
function normalizeProviders(value: string[] | undefined): string[] {
const seen = new Set<string>();
@@ -100,11 +104,26 @@ function formatOpenShellConfigIssue(issue: z.ZodIssue | undefined): string {
return issue.message;
}
function normalizeRemotePath(value: string | undefined, fallback: string): string {
function isManagedOpenShellRemotePath(value: string): boolean {
return OPEN_SHELL_MANAGED_REMOTE_ROOTS.some(
(root) => value === root || value.startsWith(`${root}/`),
);
}
export function normalizeOpenShellRemotePath(
value: string | undefined,
fallback: string,
fieldName = "remote path",
): string {
const candidate = value ?? fallback;
const normalized = path.posix.normalize(candidate.trim() || fallback);
if (!normalized.startsWith("/")) {
throw new Error(`OpenShell remote path must be absolute: ${candidate}`);
throw new Error(`OpenShell ${fieldName} must be absolute: ${candidate}`);
}
if (!isManagedOpenShellRemotePath(normalized)) {
throw new Error(
`OpenShell ${fieldName} must stay under ${OPEN_SHELL_MANAGED_REMOTE_ROOTS.join(" or ")}: ${candidate}`,
);
}
return normalized;
}
@@ -137,6 +156,8 @@ export function createOpenShellPluginConfigSchema(): OpenClawPluginConfigSchema
export function resolveOpenShellPluginConfig(value: unknown): ResolvedOpenShellPluginConfig {
if (value === undefined) {
// The built-in defaults are managed OpenShell roots, so they do not need to
// flow back through normalizeOpenShellRemotePath.
return {
mode: DEFAULT_MODE,
command: DEFAULT_COMMAND,
@@ -170,10 +191,15 @@ export function resolveOpenShellPluginConfig(value: unknown): ResolvedOpenShellP
providers: normalizeProviders(cfg.providers),
gpu: cfg.gpu ?? false,
autoProviders: cfg.autoProviders ?? true,
remoteWorkspaceDir: normalizeRemotePath(cfg.remoteWorkspaceDir, DEFAULT_REMOTE_WORKSPACE_DIR),
remoteAgentWorkspaceDir: normalizeRemotePath(
remoteWorkspaceDir: normalizeOpenShellRemotePath(
cfg.remoteWorkspaceDir,
DEFAULT_REMOTE_WORKSPACE_DIR,
"remoteWorkspaceDir",
),
remoteAgentWorkspaceDir: normalizeOpenShellRemotePath(
cfg.remoteAgentWorkspaceDir,
DEFAULT_REMOTE_AGENT_WORKSPACE_DIR,
"remoteAgentWorkspaceDir",
),
timeoutMs:
typeof cfg.timeoutSeconds === "number"

View File

@@ -12,7 +12,7 @@ import {
setBundledOpenShellCommandResolverForTest,
shellEscape,
} from "./cli.js";
import { createOpenShellPluginConfigSchema, resolveOpenShellPluginConfig } from "./config.js";
import { resolveOpenShellPluginConfig } from "./config.js";
const cliMocks = vi.hoisted(() => ({
runOpenShellCli: vi.fn(),
@@ -20,53 +20,6 @@ const cliMocks = vi.hoisted(() => ({
let createOpenShellSandboxBackendManager: typeof import("./backend.js").createOpenShellSandboxBackendManager;
describe("openshell plugin config", () => {
it("applies defaults", () => {
expect(resolveOpenShellPluginConfig(undefined)).toEqual({
mode: "mirror",
command: "openshell",
gateway: undefined,
gatewayEndpoint: undefined,
from: "openclaw",
policy: undefined,
providers: [],
gpu: false,
autoProviders: true,
remoteWorkspaceDir: "/sandbox",
remoteAgentWorkspaceDir: "/agent",
timeoutMs: 120_000,
});
});
it("accepts remote mode", () => {
expect(resolveOpenShellPluginConfig({ mode: "remote" }).mode).toBe("remote");
});
it("rejects relative remote paths", () => {
expect(() =>
resolveOpenShellPluginConfig({
remoteWorkspaceDir: "sandbox",
}),
).toThrow("OpenShell remote path must be absolute");
});
it("rejects unknown mode", () => {
expect(() =>
resolveOpenShellPluginConfig({
mode: "bogus",
}),
).toThrow("mode must be one of mirror, remote");
});
it("keeps the runtime json schema in sync with the manifest config schema", () => {
const manifest = JSON.parse(
fsSync.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"),
) as { configSchema?: unknown };
expect(createOpenShellPluginConfigSchema().jsonSchema).toEqual(manifest.configSchema);
});
});
describe("openshell cli helpers", () => {
afterEach(() => {
setBundledOpenShellCommandResolverForTest();