Files
openclaw/scripts/lib/static-extension-assets.mjs
Paul Campbell 008f04a656 feat(mxc): add Windows MXC sandbox backend (#97086)
* 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>
2026-07-12 23:07:25 -07:00

273 lines
9.6 KiB
JavaScript

// Discovers and copies static assets declared by bundled extension packages.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
function toPosixPath(value) {
return String(value ?? "").replaceAll("\\", "/");
}
function readJsonFile(filePath, fsImpl) {
return JSON.parse(fsImpl.readFileSync(filePath, "utf8"));
}
function normalizePackageRelativePath(value) {
const normalized = toPosixPath(value)
.trim()
.replace(/^\.\/+/u, "");
if (!normalized || normalized.startsWith("../") || normalized.includes("/../")) {
return "";
}
return normalized;
}
function listTrackedExtensionPackageDirs(rootDir, fsImpl) {
if (fsImpl !== fs) {
return null;
}
const result = spawnSync("git", ["ls-files", "--", ":(glob)extensions/*/package.json"], {
cwd: rootDir,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
if (result.status !== 0) {
return null;
}
return result.stdout
.split("\n")
.map((line) => toPosixPath(line.trim()))
.filter((line) => line.length > 0)
.flatMap((line) => {
const match = /^extensions\/([^/]+)\/package\.json$/u.exec(line);
if (!match?.[1]) {
return [];
}
const packageDir = path.join(rootDir, "extensions", match[1]);
return [
{
dirName: match[1],
hasPackageJson: true,
packageDir,
packageJsonPath: path.join(packageDir, "package.json"),
},
];
})
.toSorted((left, right) => left.dirName.localeCompare(right.dirName));
}
function listExtensionPackageDirs(rootDir, fsImpl) {
const trackedDirs = listTrackedExtensionPackageDirs(rootDir, fsImpl);
if (trackedDirs) {
return trackedDirs;
}
const extensionsRoot = path.join(rootDir, "extensions");
if (!fsImpl.existsSync(extensionsRoot)) {
return [];
}
return fsImpl
.readdirSync(extensionsRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => ({
dirName: entry.name,
hasPackageJson: undefined,
packageDir: path.join(extensionsRoot, entry.name),
packageJsonPath: path.join(extensionsRoot, entry.name, "package.json"),
}))
.toSorted((left, right) => left.dirName.localeCompare(right.dirName));
}
function listDistExtensionPackageDirs(rootDir, fsImpl) {
const extensionsRoot = path.join(rootDir, "dist", "extensions");
if (!fsImpl.existsSync(extensionsRoot)) {
return [];
}
return fsImpl
.readdirSync(extensionsRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name !== "node_modules")
.map((entry) => ({
dirName: entry.name,
packageDir: path.join(extensionsRoot, entry.name),
}))
.toSorted((left, right) => left.dirName.localeCompare(right.dirName));
}
function readPackageStaticAssetEntries(packageJson) {
const entries = packageJson.openclaw?.build?.staticAssets;
return Array.isArray(entries) ? entries : [];
}
// External plugins (`bundledDist: false`) own their own dist and are excluded
// from core dist, so their static assets must not be discovered for core
// runtime postbuild copies.
function isExternalDistPackage(packageJson) {
return packageJson.openclaw?.build?.bundledDist === false;
}
/**
* Discovers static asset copy specs from extension package metadata.
*
* External plugins (`bundledDist: false`) are skipped by default so their
* launchers are not copied into core dist. Per-package plugin builds pass
* `includeExternalPlugins` to still emit their own static assets.
*/
export function discoverStaticExtensionAssets(params = {}) {
const rootDir = params.rootDir ?? process.cwd();
const fsImpl = params.fs ?? fs;
const includeExternalPlugins = params.includeExternalPlugins ?? false;
const assets = [];
for (const { dirName, hasPackageJson, packageJsonPath } of listExtensionPackageDirs(
rootDir,
fsImpl,
)) {
if (!(hasPackageJson ?? fsImpl.existsSync(packageJsonPath))) {
continue;
}
const packageJson = readJsonFile(packageJsonPath, fsImpl);
if (!includeExternalPlugins && isExternalDistPackage(packageJson)) {
continue;
}
for (const entry of readPackageStaticAssetEntries(packageJson)) {
const source = normalizePackageRelativePath(entry?.source);
const output = normalizePackageRelativePath(entry?.output);
if (!source || !output) {
continue;
}
assets.push({
pluginDir: dirName,
src: toPosixPath(path.posix.join("extensions", dirName, source)),
dest: toPosixPath(path.posix.join("dist", "extensions", dirName, output)),
});
}
}
return assets.toSorted((left, right) => left.dest.localeCompare(right.dest));
}
function discoverStaticExtensionRuntimeOverlayAssets(params = {}) {
const rootDir = params.rootDir ?? process.cwd();
const fsImpl = params.fs ?? fs;
const assetsByDest = new Map();
for (const asset of params.assets ?? discoverStaticExtensionAssets({ rootDir, fs: fsImpl })) {
assetsByDest.set(asset.dest, asset);
}
for (const { dirName, packageDir } of listDistExtensionPackageDirs(rootDir, fsImpl)) {
const packageJsonPath = path.join(packageDir, "package.json");
if (!fsImpl.existsSync(packageJsonPath)) {
continue;
}
const packageJson = readJsonFile(packageJsonPath, fsImpl);
for (const entry of readPackageStaticAssetEntries(packageJson)) {
const output = normalizePackageRelativePath(entry?.output);
if (!output) {
continue;
}
const dest = toPosixPath(path.posix.join("dist", "extensions", dirName, output));
if (!assetsByDest.has(dest)) {
assetsByDest.set(dest, { pluginDir: dirName, src: dest, dest });
}
}
}
return [...assetsByDest.values()].toSorted((left, right) => left.dest.localeCompare(right.dest));
}
/**
* Lists generated dist output paths for declared static extension assets.
*/
export function listStaticExtensionAssetOutputs(params = {}) {
const assets = params.assets ?? discoverStaticExtensionAssets(params);
return assets
.map(({ dest }) => dest.replace(/\\/g, "/"))
.toSorted((left, right) => left.localeCompare(right));
}
/**
* Lists source file paths for declared static extension assets.
*/
export function listStaticExtensionAssetSources(params = {}) {
const assets = params.assets ?? discoverStaticExtensionAssets(params);
return assets
.map(({ src }) => src.replace(/\\/g, "/"))
.toSorted((left, right) => left.localeCompare(right));
}
/**
* Copies declared static extension assets from source packages into root dist.
*/
export function copyStaticExtensionAssets(params = {}) {
const rootDir = params.rootDir ?? process.cwd();
const fsImpl = params.fs ?? fs;
const assets = params.assets ?? discoverStaticExtensionAssets({ rootDir, fs: fsImpl });
const warn = params.warn ?? console.warn;
for (const { src, dest } of assets) {
const srcPath = path.join(rootDir, src);
const destPath = path.join(rootDir, dest);
if (fsImpl.existsSync(srcPath)) {
fsImpl.mkdirSync(path.dirname(destPath), { recursive: true });
fsImpl.copyFileSync(srcPath, destPath);
} else {
warn(`[runtime-postbuild] static asset not found, skipping: ${src}`);
}
}
}
/**
* Copies static assets into the dist-runtime overlay from source or root dist.
*/
export function copyStaticExtensionAssetsToRuntimeOverlay(params = {}) {
const rootDir = params.rootDir ?? process.cwd();
const fsImpl = params.fs ?? fs;
const assets = discoverStaticExtensionRuntimeOverlayAssets({ ...params, rootDir, fs: fsImpl });
const runtimeExtensionsRoot = path.join(rootDir, "dist-runtime", "extensions");
if (!fsImpl.existsSync(runtimeExtensionsRoot)) {
return;
}
const warn = params.warn ?? console.warn;
for (const { src, dest } of assets) {
const normalizedDest = toPosixPath(dest);
if (!normalizedDest.startsWith("dist/extensions/")) {
continue;
}
const srcPath = path.join(rootDir, src);
const distPath = path.join(rootDir, dest);
const copySourcePath = fsImpl.existsSync(srcPath) ? srcPath : distPath;
const destPath = path.join(rootDir, "dist-runtime", normalizedDest.slice("dist/".length));
if (fsImpl.existsSync(copySourcePath)) {
fsImpl.mkdirSync(path.dirname(destPath), { recursive: true });
fsImpl.copyFileSync(copySourcePath, destPath);
} else {
warn(`[runtime-postbuild] static asset not found, skipping: ${src}`);
}
}
}
/**
* Copies declared static assets for one package runtime build.
*/
export function copyStaticExtensionAssetsForPackage(params) {
const rootDir = params.rootDir ?? process.cwd();
const fsImpl = params.fs ?? fs;
const assets =
params.assets ??
discoverStaticExtensionAssets({ rootDir, fs: fsImpl, includeExternalPlugins: true });
const packagePrefix = `extensions/${params.pluginDir}/`;
const rootDistPrefix = `dist/extensions/${params.pluginDir}/`;
const copied = [];
for (const { src, dest } of assets) {
const normalizedSrc = src.replaceAll("\\", "/");
const normalizedDest = dest.replaceAll("\\", "/");
if (!normalizedSrc.startsWith(packagePrefix) || !normalizedDest.startsWith(rootDistPrefix)) {
continue;
}
const srcPath = path.join(rootDir, src);
if (!fsImpl.existsSync(srcPath)) {
continue;
}
const packageRelativeDest = normalizedDest.slice(rootDistPrefix.length);
const destPath = path.join(rootDir, packagePrefix, "dist", packageRelativeDest);
fsImpl.mkdirSync(path.dirname(destPath), { recursive: true });
fsImpl.copyFileSync(srcPath, destPath);
copied.push(`dist/${packageRelativeDest}`);
}
return copied.toSorted((left, right) => left.localeCompare(right));
}