mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 22:24:31 +00:00
fix: make windows CI path handling deterministic
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath, URL } from "node:url";
|
||||||
import { isNotFoundPathError, isPathInside } from "../infra/path-guards.js";
|
import { isNotFoundPathError, isPathInside } from "../infra/path-guards.js";
|
||||||
|
|
||||||
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
||||||
@@ -85,10 +85,18 @@ export async function resolveSandboxedMediaSource(params: {
|
|||||||
}
|
}
|
||||||
let candidate = raw;
|
let candidate = raw;
|
||||||
if (/^file:\/\//i.test(candidate)) {
|
if (/^file:\/\//i.test(candidate)) {
|
||||||
try {
|
const workspaceMappedFromUrl = mapContainerWorkspaceFileUrl({
|
||||||
candidate = fileURLToPath(candidate);
|
fileUrl: candidate,
|
||||||
} catch {
|
sandboxRoot: params.sandboxRoot,
|
||||||
throw new Error(`Invalid file:// URL for sandboxed media: ${raw}`);
|
});
|
||||||
|
if (workspaceMappedFromUrl) {
|
||||||
|
candidate = workspaceMappedFromUrl;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
candidate = fileURLToPath(candidate);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Invalid file:// URL for sandboxed media: ${raw}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const containerWorkspaceMapped = mapContainerWorkspacePath({
|
const containerWorkspaceMapped = mapContainerWorkspacePath({
|
||||||
@@ -113,6 +121,34 @@ export async function resolveSandboxedMediaSource(params: {
|
|||||||
return sandboxResult.resolved;
|
return sandboxResult.resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapContainerWorkspaceFileUrl(params: {
|
||||||
|
fileUrl: string;
|
||||||
|
sandboxRoot: string;
|
||||||
|
}): string | undefined {
|
||||||
|
let parsed: URL;
|
||||||
|
try {
|
||||||
|
parsed = new URL(params.fileUrl);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (parsed.protocol !== "file:") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// Sandbox paths are Linux-style (/workspace/*). Parse the URL path directly so
|
||||||
|
// Windows hosts can still accept file:///workspace/... media references.
|
||||||
|
const normalizedPathname = decodeURIComponent(parsed.pathname).replace(/\\/g, "/");
|
||||||
|
if (
|
||||||
|
normalizedPathname !== SANDBOX_CONTAINER_WORKDIR &&
|
||||||
|
!normalizedPathname.startsWith(`${SANDBOX_CONTAINER_WORKDIR}/`)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return mapContainerWorkspacePath({
|
||||||
|
candidate: normalizedPathname,
|
||||||
|
sandboxRoot: params.sandboxRoot,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function mapContainerWorkspacePath(params: {
|
function mapContainerWorkspacePath(params: {
|
||||||
candidate: string;
|
candidate: string;
|
||||||
sandboxRoot: string;
|
sandboxRoot: string;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
isInterpreterLikeSafeBin,
|
isInterpreterLikeSafeBin,
|
||||||
@@ -72,16 +73,18 @@ describe("exec safe-bin runtime policy", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("merges explicit safe-bin trusted dirs from global and local config", () => {
|
it("merges explicit safe-bin trusted dirs from global and local config", () => {
|
||||||
|
const customDir = path.join(path.sep, "custom", "bin");
|
||||||
|
const agentDir = path.join(path.sep, "agent", "bin");
|
||||||
const policy = resolveExecSafeBinRuntimePolicy({
|
const policy = resolveExecSafeBinRuntimePolicy({
|
||||||
global: {
|
global: {
|
||||||
safeBinTrustedDirs: [" /custom/bin ", "/custom/bin"],
|
safeBinTrustedDirs: [` ${customDir} `, customDir],
|
||||||
},
|
},
|
||||||
local: {
|
local: {
|
||||||
safeBinTrustedDirs: ["/agent/bin"],
|
safeBinTrustedDirs: [agentDir],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(policy.trustedSafeBinDirs.has("/custom/bin")).toBe(true);
|
expect(policy.trustedSafeBinDirs.has(path.resolve(customDir))).toBe(true);
|
||||||
expect(policy.trustedSafeBinDirs.has("/agent/bin")).toBe(true);
|
expect(policy.trustedSafeBinDirs.has(path.resolve(agentDir))).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,17 +51,19 @@ describe("withExtractedArchiveRoot", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("extracts archive and passes root directory to callback", async () => {
|
it("extracts archive and passes root directory to callback", async () => {
|
||||||
|
const tmpRoot = path.join(path.sep, "tmp", "openclaw-install-flow");
|
||||||
|
const archivePath = path.join(path.sep, "tmp", "plugin.tgz");
|
||||||
|
const extractDir = path.join(tmpRoot, "extract");
|
||||||
|
const packageRoot = path.join(extractDir, "package");
|
||||||
const withTempDirSpy = vi
|
const withTempDirSpy = vi
|
||||||
.spyOn(installSource, "withTempDir")
|
.spyOn(installSource, "withTempDir")
|
||||||
.mockImplementation(async (_prefix, fn) => await fn("/tmp/openclaw-install-flow"));
|
.mockImplementation(async (_prefix, fn) => await fn(tmpRoot));
|
||||||
const extractSpy = vi.spyOn(archive, "extractArchive").mockResolvedValue(undefined);
|
const extractSpy = vi.spyOn(archive, "extractArchive").mockResolvedValue(undefined);
|
||||||
const resolveRootSpy = vi
|
const resolveRootSpy = vi.spyOn(archive, "resolvePackedRootDir").mockResolvedValue(packageRoot);
|
||||||
.spyOn(archive, "resolvePackedRootDir")
|
|
||||||
.mockResolvedValue("/tmp/openclaw-install-flow/extract/package");
|
|
||||||
|
|
||||||
const onExtracted = vi.fn(async (rootDir: string) => ({ ok: true as const, rootDir }));
|
const onExtracted = vi.fn(async (rootDir: string) => ({ ok: true as const, rootDir }));
|
||||||
const result = await withExtractedArchiveRoot({
|
const result = await withExtractedArchiveRoot({
|
||||||
archivePath: "/tmp/plugin.tgz",
|
archivePath,
|
||||||
tempDirPrefix: "openclaw-plugin-",
|
tempDirPrefix: "openclaw-plugin-",
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
onExtracted,
|
onExtracted,
|
||||||
@@ -70,14 +72,14 @@ describe("withExtractedArchiveRoot", () => {
|
|||||||
expect(withTempDirSpy).toHaveBeenCalledWith("openclaw-plugin-", expect.any(Function));
|
expect(withTempDirSpy).toHaveBeenCalledWith("openclaw-plugin-", expect.any(Function));
|
||||||
expect(extractSpy).toHaveBeenCalledWith(
|
expect(extractSpy).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
archivePath: "/tmp/plugin.tgz",
|
archivePath,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(resolveRootSpy).toHaveBeenCalledWith("/tmp/openclaw-install-flow/extract");
|
expect(resolveRootSpy).toHaveBeenCalledWith(extractDir);
|
||||||
expect(onExtracted).toHaveBeenCalledWith("/tmp/openclaw-install-flow/extract/package");
|
expect(onExtracted).toHaveBeenCalledWith(packageRoot);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
ok: true,
|
ok: true,
|
||||||
rootDir: "/tmp/openclaw-install-flow/extract/package",
|
rootDir: packageRoot,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user