mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
- Cache host libc detection and load TypeScript 5 only when needed. - Exclude debug payloads from release runtime packs and vendor archives. - Preserve native self-hosting, portable diagnostics, and reliable Sandbox execution.
273 lines
9.2 KiB
JavaScript
273 lines
9.2 KiB
JavaScript
import { loadEnvFile } from "node:process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const customImageExample = "vcr.vercel.com/<team>/<project>/<repository>:<tag>";
|
|
export const defaultSandboxImage = "vercel/sandbox/universal";
|
|
const localEnv = fileURLToPath(new URL("../.env.local", import.meta.url));
|
|
let loadedLocalEnv = false;
|
|
|
|
function loadLocalEnv() {
|
|
if (loadedLocalEnv) return;
|
|
loadedLocalEnv = true;
|
|
try {
|
|
// Node preserves variables already present in the process environment,
|
|
// allowing an agent or shell to override values from this local file.
|
|
loadEnvFile(localEnv);
|
|
} catch (error) {
|
|
if (error?.code !== "ENOENT") throw error;
|
|
}
|
|
}
|
|
|
|
export function sandboxRunnerConfig(env) {
|
|
loadLocalEnv();
|
|
const source = env ?? process.env;
|
|
const sandboxTimeout = source.SCRIPTC_SANDBOX_TIMEOUT ?? "45m";
|
|
const duration = /^(\d+)\s*(ms|s|m|h)$/.exec(sandboxTimeout.trim());
|
|
const units = { ms: 1, s: 1000, m: 60_000, h: 3_600_000 };
|
|
const sandboxTimeoutMs = duration ? Number(duration[1]) * units[duration[2]] : 0;
|
|
if (!Number.isSafeInteger(sandboxTimeoutMs) || sandboxTimeoutMs <= 0 || sandboxTimeoutMs > 2_147_483_647) {
|
|
throw new Error("SCRIPTC_SANDBOX_TIMEOUT must be a positive duration such as 45m or 2h");
|
|
}
|
|
return {
|
|
vcpus: source.SCRIPTC_SANDBOX_VCPUS ?? "8",
|
|
testWorkers: source.SCRIPTC_TEST_WORKERS ?? "4",
|
|
localTestWorkers: source.SCRIPTC_LOCAL_TEST_WORKERS ?? "2",
|
|
localCaseShards: source.SCRIPTC_LOCAL_CASE_SHARDS ?? "2",
|
|
sandboxTimeout,
|
|
sandboxTimeoutMs,
|
|
};
|
|
}
|
|
|
|
/** Select Sandbox credentials and scope without coupling either to the image.
|
|
* OIDC is project-scoped and takes precedence over legacy access tokens and
|
|
* their explicit environment scope. */
|
|
export function sandboxVercelConfig(env) {
|
|
loadLocalEnv();
|
|
const source = env ?? process.env;
|
|
const oidcToken = source.VERCEL_OIDC_TOKEN?.trim();
|
|
const configuredTeam = source.VERCEL_TEAM_ID?.trim();
|
|
const configuredProject = source.VERCEL_PROJECT_ID?.trim();
|
|
if (!oidcToken && Boolean(configuredTeam) !== Boolean(configuredProject)) {
|
|
throw new Error(
|
|
"VERCEL_TEAM_ID and VERCEL_PROJECT_ID must be set together; " +
|
|
"Sandbox scope is independent of SCRIPTC_SANDBOX_IMAGE",
|
|
);
|
|
}
|
|
const team = oidcToken ? undefined : configuredTeam;
|
|
const project = oidcToken ? undefined : configuredProject;
|
|
|
|
const accessToken = source.VERCEL_TOKEN?.trim() || source.VERCEL_AUTH_TOKEN?.trim();
|
|
const accessTokenSource = source.VERCEL_TOKEN?.trim()
|
|
? "VERCEL_TOKEN"
|
|
: source.VERCEL_AUTH_TOKEN?.trim()
|
|
? "VERCEL_AUTH_TOKEN"
|
|
: undefined;
|
|
const authSource = oidcToken
|
|
? "VERCEL_OIDC_TOKEN"
|
|
: accessToken
|
|
? accessTokenSource
|
|
: "Vercel CLI login";
|
|
if (accessToken && !oidcToken && (!team || !project)) {
|
|
throw new Error(
|
|
`${authSource} requires VERCEL_TEAM_ID and VERCEL_PROJECT_ID for Sandbox access`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
accessToken,
|
|
accessTokenSource,
|
|
authSource,
|
|
authToken: oidcToken || accessToken,
|
|
oidc: Boolean(oidcToken),
|
|
project,
|
|
scopeArgs: team && project ? ["--scope", team, "--project", project] : [],
|
|
scopeSource: oidcToken
|
|
? "VERCEL_OIDC_TOKEN claims"
|
|
: team && project
|
|
? "VERCEL_TEAM_ID + VERCEL_PROJECT_ID"
|
|
: "linked/default Vercel CLI project",
|
|
team,
|
|
};
|
|
}
|
|
|
|
/** Build a child environment that makes the selected authentication source
|
|
* unambiguous. The Vercel wrapper otherwise prefers a stored/access token to
|
|
* VERCEL_OIDC_TOKEN when both are available. */
|
|
export function sandboxVercelEnvironment(config, env) {
|
|
const childEnv = { ...(env ?? process.env) };
|
|
if (config.authToken) childEnv.VERCEL_AUTH_TOKEN = config.authToken;
|
|
if (config.oidc) delete childEnv.VERCEL_TOKEN;
|
|
return childEnv;
|
|
}
|
|
|
|
/** Adapt the selected Sandbox credential to the legacy VCR command surface.
|
|
* Unlike the Sandbox CLI, `vercel vcr` rejects JWTs in VERCEL_TOKEN and cannot
|
|
* infer registry scope from OIDC claims. OIDC therefore supplies only VCR's
|
|
* scope; authentication uses an explicit access token or the Vercel CLI login. */
|
|
export function sandboxVcrConfig(config, env) {
|
|
const childEnv = { ...(env ?? process.env) };
|
|
delete childEnv.VERCEL_AUTH_TOKEN;
|
|
let authSource = "Vercel CLI login";
|
|
const vcrAccessToken = /^\w+$/.test(config.accessToken ?? "")
|
|
? config.accessToken
|
|
: undefined;
|
|
if (config.accessToken && !vcrAccessToken && !config.oidc) {
|
|
throw new Error(
|
|
`${config.accessTokenSource} is not a VCR-compatible access token; ` +
|
|
"use VERCEL_TOKEN with a classic access token or run `vercel login`",
|
|
);
|
|
}
|
|
if (vcrAccessToken) {
|
|
childEnv.VERCEL_TOKEN = vcrAccessToken;
|
|
authSource = config.accessTokenSource;
|
|
} else {
|
|
delete childEnv.VERCEL_TOKEN;
|
|
}
|
|
|
|
if (config.team && config.project) {
|
|
return {
|
|
authSource,
|
|
env: childEnv,
|
|
scopeArgs: ["--scope", config.team, "--project", config.project],
|
|
scopeSource: "VERCEL_TEAM_ID + VERCEL_PROJECT_ID",
|
|
};
|
|
}
|
|
if (!config.oidc) {
|
|
return {
|
|
authSource,
|
|
env: childEnv,
|
|
scopeArgs: [],
|
|
scopeSource: "linked/default Vercel CLI project",
|
|
};
|
|
}
|
|
|
|
let claims;
|
|
try {
|
|
const segments = config.authToken?.split(".") ?? [];
|
|
if (segments.length !== 3 || !segments[1]) throw new Error("token is not a JWT");
|
|
claims = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
|
|
} catch (cause) {
|
|
throw new Error(
|
|
"VERCEL_OIDC_TOKEN must be a JWT with owner_id and project_id claims for VCR access",
|
|
{ cause },
|
|
);
|
|
}
|
|
const team = typeof claims.owner_id === "string" ? claims.owner_id.trim() : "";
|
|
const project = typeof claims.project_id === "string" ? claims.project_id.trim() : "";
|
|
if (!team || !project) {
|
|
throw new Error(
|
|
"VERCEL_OIDC_TOKEN must contain owner_id and project_id claims for VCR access",
|
|
);
|
|
}
|
|
return {
|
|
authSource,
|
|
env: childEnv,
|
|
scopeArgs: ["--scope", team, "--project", project],
|
|
scopeSource: "VERCEL_OIDC_TOKEN claims",
|
|
};
|
|
}
|
|
|
|
export function sandboxBootstrapCommand(customImage) {
|
|
if (customImage) return undefined;
|
|
return {
|
|
install: {
|
|
args: ["scripts/sandbox-bootstrap.sh"],
|
|
command: "sh",
|
|
workdir: "/workspace",
|
|
},
|
|
prepareWorkspace: {
|
|
args: [
|
|
"-c",
|
|
"sudo mkdir -p /workspace && sudo chown \"$(id -u):$(id -g)\" /workspace",
|
|
],
|
|
command: "sh",
|
|
workdir: "/vercel",
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Reserve one Vitest worker for each concurrently scheduled side process
|
|
* without ever starving the case-sharded corpus or exceeding the configured
|
|
* per-Sandbox worker budget. Extra side processes share the reserved slots. */
|
|
export function sandboxTestWorkerAllocation(workerCount, sideTaskCount) {
|
|
if (!Number.isInteger(workerCount) || workerCount < 1) {
|
|
throw new Error("workerCount must be a positive integer");
|
|
}
|
|
if (!Number.isInteger(sideTaskCount) || sideTaskCount < 0) {
|
|
throw new Error("sideTaskCount must be a non-negative integer");
|
|
}
|
|
const sideConcurrency = Math.min(sideTaskCount, Math.max(0, workerCount - 1));
|
|
return {
|
|
caseWorkers: workerCount - sideConcurrency,
|
|
sideConcurrency,
|
|
};
|
|
}
|
|
|
|
export function sandboxImageConfig(env) {
|
|
loadLocalEnv();
|
|
const source = env ?? process.env;
|
|
const configuredReference = source.SCRIPTC_SANDBOX_IMAGE?.trim();
|
|
if (!configuredReference) {
|
|
return {
|
|
custom: false,
|
|
reference: defaultSandboxImage,
|
|
sandboxImage: defaultSandboxImage,
|
|
};
|
|
}
|
|
|
|
const match = /^vcr\.vercel\.com\/([^/]+)\/([^/]+)\/([^/:]+):([^/:]+)$/.exec(
|
|
configuredReference,
|
|
);
|
|
if (!match) {
|
|
throw new Error(
|
|
`SCRIPTC_SANDBOX_IMAGE must be a fully qualified VCR image (${customImageExample})`,
|
|
);
|
|
}
|
|
|
|
const [, , , repository, tag] = match;
|
|
return {
|
|
custom: true,
|
|
reference: configuredReference,
|
|
repository,
|
|
tag,
|
|
// Preserve the full reference: Sandbox scope comes from Vercel auth,
|
|
// never from path segments in this image name.
|
|
sandboxImage: configuredReference,
|
|
};
|
|
}
|
|
|
|
export function requiredSandboxImageConfig(env) {
|
|
const config = sandboxImageConfig(env);
|
|
const reference = (env ?? process.env).SCRIPTC_SANDBOX_IMAGE?.trim();
|
|
if (!reference) {
|
|
throw new Error(
|
|
`SCRIPTC_SANDBOX_IMAGE is required to build an image (for example: ${customImageExample})`,
|
|
);
|
|
}
|
|
return config;
|
|
}
|
|
|
|
/** A prepared snapshot reuses the same pinned tools as a custom image.
|
|
* The runner still replaces the source tree and rebuilds native artifacts. */
|
|
export function sandboxTestSourceConfig(env) {
|
|
loadLocalEnv();
|
|
const source = env ?? process.env;
|
|
const snapshot = source.SCRIPTC_SANDBOX_SNAPSHOT?.trim();
|
|
if (snapshot) {
|
|
if (source.SCRIPTC_SANDBOX_IMAGE?.trim()) {
|
|
throw new Error("Set only one of SCRIPTC_SANDBOX_SNAPSHOT and SCRIPTC_SANDBOX_IMAGE");
|
|
}
|
|
if (!/^snap_[A-Za-z0-9]+$/.test(snapshot)) {
|
|
throw new Error("SCRIPTC_SANDBOX_SNAPSHOT must be a Sandbox snapshot ID (snap_...)");
|
|
}
|
|
return { prepared: true, reference: snapshot, description: "prepared snapshot", createArgs: ["--snapshot", snapshot] };
|
|
}
|
|
const image = sandboxImageConfig(source);
|
|
return {
|
|
prepared: image.custom,
|
|
reference: image.sandboxImage,
|
|
description: image.custom ? "custom VCR image" : "managed fallback image",
|
|
createArgs: ["--image", image.sandboxImage],
|
|
};
|
|
}
|