Compare commits

..

1 Commits

6 changed files with 96 additions and 94 deletions

View File

@@ -0,0 +1 @@
- **fix(build):** stop Turbopack from dead-code-eliminating the Windows Tailscale branches of `src/lib/tailscaleTunnel.ts` in the published build (#10293). The release `dist` is bundled on a Linux runner, and the bundler constant-folds `process.platform`, pruning every non-Linux branch — the Windows installers shipped with no `where` lookup, an always-injected `--socket`, and a lost `net start Tailscale`/windows-default-binary path. The module now reads the platform at runtime via `os.platform()` (a function call a bundler cannot fold), so the Windows branches survive on any build machine; a vitest regression test mocking `os.platform()``win32` guards the anti-fold invariant (RED before, GREEN after).

View File

@@ -1 +0,0 @@
- fix(providers): GitLab Duo falls back to the public Code Suggestions endpoint when direct_access returns 401 (#10365)

View File

@@ -583,20 +583,10 @@ export class GitlabExecutor extends BaseExecutor {
}
if (response.status === 401) {
if (input.log) {
input.log.warn(
"GITLAB-DUO",
"direct_access exchange rejected (401); falling back to public completions endpoint"
);
}
return {
target: {
mode: "monolith",
url: endpoints.publicCompletionsUrl,
headers: buildMonolithHeaders(credentials.accessToken || null),
},
target: null,
credentials,
errorResponse: null,
errorResponse: toOpenAIError(401, "GitLab Duo direct access token request was rejected"),
};
}

View File

@@ -15,9 +15,15 @@ const execFileAsync = promisify(execFile);
const WINDOWS_TAILSCALE_BIN = "C:\\Program Files\\Tailscale\\tailscale.exe";
const WINDOWS_TAILSCALED_BIN = "C:\\Program Files\\Tailscale\\tailscaled.exe";
const IS_MAC = process.platform === "darwin";
const IS_LINUX = process.platform === "linux";
const IS_WINDOWS = process.platform === "win32";
// Runtime platform getter. A bundler (Turbopack in `next build`) constant-folds
// `process.platform` to the BUILD machine's value on a non-Windows runner and prunes
// the other branches as dead code (#10293). `os.platform()` is a runtime call a
// bundler cannot fold, so Windows/macOS/Linux branches survive on any build machine.
function getCurrentPlatform(): NodeJS.Platform {
return os.platform();
}
const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`;
const LOGIN_TIMEOUT_MS = 15000;
const FUNNEL_TIMEOUT_MS = 30000;
@@ -35,12 +41,7 @@ type JsonRecord = Record<string, unknown>;
export type TailscaleTunnelInstallSource = "managed" | "path" | "env" | "windows-default";
export type TailscaleTunnelPhase =
| "unsupported"
| "not_installed"
| "needs_login"
| "stopped"
| "running"
| "error";
"unsupported" | "not_installed" | "needs_login" | "stopped" | "running" | "error";
type PersistedTailscaleState = {
binaryPath?: string | null;
@@ -61,8 +62,7 @@ type BinaryResolution = {
type TailscaleLoginResult = { alreadyLoggedIn: true } | { authUrl: string };
type TailscaleFunnelResult =
| { tunnelUrl: string }
| { funnelNotEnabled: true; enableUrl: string | null };
{ tunnelUrl: string } | { funnelNotEnabled: true; enableUrl: string | null };
export type TailscaleCheckStatus = {
supported: boolean;
@@ -124,7 +124,7 @@ function shellEscape(value: string) {
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
}
function isSupportedPlatform(platform = process.platform) {
function isSupportedPlatform(platform = os.platform()) {
return platform === "darwin" || platform === "linux" || platform === "win32";
}
@@ -132,7 +132,7 @@ function getTailscaleDir() {
return path.join(resolveDataDir(), "tailscale");
}
function getManagedBinaryPath(platform = process.platform) {
function getManagedBinaryPath(platform = os.platform()) {
return path.join(getTailscaleDir(), "bin", platform === "win32" ? "tailscale.exe" : "tailscale");
}
@@ -212,7 +212,7 @@ function getTailscaleApiUrl(tunnelUrl: string | null) {
}
async function resolvePathCommand(command: string) {
const lookupCommand = process.platform === "win32" ? "where" : "which";
const lookupCommand = os.platform() === "win32" ? "where" : "which";
try {
const { stdout } = await execFileAsync(lookupCommand, [command], {
timeout: 3000,
@@ -248,7 +248,7 @@ async function resolveBinary(): Promise<BinaryResolution> {
return { binaryPath: pathBinary, installSource: "path", managedInstall: false };
}
if (IS_WINDOWS && fs.existsSync(WINDOWS_TAILSCALE_BIN)) {
if (getCurrentPlatform() === "win32" && fs.existsSync(WINDOWS_TAILSCALE_BIN)) {
return {
binaryPath: WINDOWS_TAILSCALE_BIN,
installSource: "windows-default",
@@ -263,7 +263,7 @@ async function resolveDaemonBinary(tailscaleBinaryPath: string | null) {
const envPath = toNonEmptyString(process.env.TAILSCALED_BIN);
if (envPath && fs.existsSync(envPath)) return envPath;
const daemonFilename = process.platform === "win32" ? "tailscaled.exe" : "tailscaled";
const daemonFilename = os.platform() === "win32" ? "tailscaled.exe" : "tailscaled";
const siblingDir = tailscaleBinaryPath ? path.dirname(tailscaleBinaryPath) : null;
// path.format avoids the path.join/resolve pattern flagged by CWE-22 linters;
// siblingDir is path.dirname of a trusted system binary from resolveBinary(), not user input.
@@ -273,7 +273,8 @@ async function resolveDaemonBinary(tailscaleBinaryPath: string | null) {
const pathBinary = await resolvePathCommand("tailscaled");
if (pathBinary) return pathBinary;
if (IS_WINDOWS && fs.existsSync(WINDOWS_TAILSCALED_BIN)) return WINDOWS_TAILSCALED_BIN;
if (getCurrentPlatform() === "win32" && fs.existsSync(WINDOWS_TAILSCALED_BIN))
return WINDOWS_TAILSCALED_BIN;
return null;
}
@@ -298,7 +299,9 @@ async function getActiveSocketPath(): Promise<string> {
}
// Check system sockets first
const systemSocket = IS_LINUX ? SYSTEM_SOCKET_LINUX : IS_MAC ? SYSTEM_SOCKET_MAC : null;
const platform = getCurrentPlatform();
const systemSocket =
platform === "linux" ? SYSTEM_SOCKET_LINUX : platform === "darwin" ? SYSTEM_SOCKET_MAC : null;
if (systemSocket && fs.existsSync(systemSocket)) {
_cachedActiveSocket = systemSocket;
_cachedActiveSocketTimestamp = now;
@@ -314,7 +317,9 @@ async function getActiveSocketPath(): Promise<string> {
/** Synchronous check: is the system daemon socket available? */
function isSystemDaemonAvailable(): boolean {
const systemSocket = IS_LINUX ? SYSTEM_SOCKET_LINUX : IS_MAC ? SYSTEM_SOCKET_MAC : null;
const platform = getCurrentPlatform();
const systemSocket =
platform === "linux" ? SYSTEM_SOCKET_LINUX : platform === "darwin" ? SYSTEM_SOCKET_MAC : null;
return Boolean(systemSocket && fs.existsSync(systemSocket));
}
@@ -341,19 +346,20 @@ export function tailscaleUpArgs(hostname?: string, authKey?: string): string[] {
}
async function buildTailscaleArgs(...args: string[]) {
if (IS_WINDOWS) return args;
if (getCurrentPlatform() === "win32") return args;
const socket = await getActiveSocketPath();
return ["--socket", socket, ...args];
}
/** Synchronous variant for places that cannot await */
function buildTailscaleArgsSync(...args: string[]) {
if (IS_WINDOWS) return args;
if (getCurrentPlatform() === "win32") return args;
// Use cached socket or default to system socket if available
const platform = getCurrentPlatform();
const socket =
_cachedActiveSocket ||
(isSystemDaemonAvailable()
? IS_LINUX
? platform === "linux"
? SYSTEM_SOCKET_LINUX
: SYSTEM_SOCKET_MAC
: getTailscaleSocketPath());
@@ -443,7 +449,7 @@ function getLastError(state: PersistedTailscaleState) {
}
async function hasBrew() {
if (!IS_MAC) return false;
if (getCurrentPlatform() !== "darwin") return false;
try {
await execFileAsync("which", ["brew"], {
timeout: 3000,
@@ -487,7 +493,7 @@ export async function getTailscaleCheckStatus(): Promise<TailscaleCheckStatus> {
running: isFunnelRunning(funnelPayload),
tunnelUrl,
apiUrl: getTailscaleApiUrl(tunnelUrl),
platform: process.platform,
platform: os.platform(),
brewAvailable,
lastError: getLastError(state),
pid: await readPidFile(),
@@ -561,7 +567,7 @@ export async function startTailscaleDaemon({
return { started: false };
}
if (IS_WINDOWS) {
if (getCurrentPlatform() === "win32") {
try {
await execFileAsync("net", ["start", "Tailscale"], {
timeout: 10000,
@@ -816,7 +822,7 @@ export async function stopTailscaleDaemon({
}
}
if (!IS_WINDOWS) {
if (getCurrentPlatform() !== "win32") {
try {
await execFileAsync("pkill", ["-x", "tailscaled"], {
timeout: 3000,
@@ -1155,7 +1161,7 @@ export async function installTailscale({
onProgress?: (message: string) => void;
} = {}) {
if (!isSupportedPlatform()) {
throw new Error(`Unsupported platform for Tailscale install: ${process.platform}`);
throw new Error(`Unsupported platform for Tailscale install: ${os.platform()}`);
}
const password = toNonEmptyString(sudoPassword) || getCachedPassword() || "";
@@ -1167,13 +1173,13 @@ export async function installTailscale({
const existingBinary = await resolveBinary();
if (existingBinary.binaryPath) {
onProgress?.("Tailscale is already installed.");
} else if (IS_WINDOWS) {
} else if (getCurrentPlatform() === "win32") {
onProgress?.("Downloading and installing Tailscale for Windows...");
await installTailscaleWindows(onProgress);
} else if (IS_MAC) {
} else if (getCurrentPlatform() === "darwin") {
onProgress?.("Installing Tailscale on macOS...");
await installTailscaleMac(password, onProgress);
} else if (IS_LINUX) {
} else if (getCurrentPlatform() === "linux") {
onProgress?.("Installing Tailscale on Linux...");
await installTailscaleLinux(password, onProgress);
}

View File

@@ -261,54 +261,3 @@ test("GitlabExecutor falls back to the public Code Suggestions endpoint when dir
globalThis.fetch = originalFetch;
}
});
// #10365: a 401 from the direct_access exchange must ALSO fall back to the public
// Code Suggestions completions endpoint (same resilience as the 403-disabled case
// above), instead of surfacing an opaque 401 token error with no fallback.
test("GitlabExecutor falls back to the public Code Suggestions endpoint when direct_access returns 401", async () => {
const executor = getExecutor("gitlab-duo") as GitlabExecutor;
const originalFetch = globalThis.fetch;
const calls: string[] = [];
globalThis.fetch = async (url) => {
calls.push(String(url));
if (String(url) === "https://gitlab.example.com/api/v4/code_suggestions/direct_access") {
return jsonResponse({ error: "invalid_token" }, 401);
}
return jsonResponse({
model: { name: "code-gecko" },
choices: [{ text: "monolith fallback works" }],
});
};
try {
const result = await executor.execute({
model: "gitlab-duo-code-suggestions",
body: {
messages: [{ role: "user", content: "Say hello" }],
},
stream: false,
credentials: {
accessToken: "oauth-access",
providerSpecificData: {
baseUrl: "https://gitlab.example.com",
},
},
signal: AbortSignal.timeout(10_000),
log: null,
});
assert.deepEqual(calls, [
"https://gitlab.example.com/api/v4/code_suggestions/direct_access",
"https://gitlab.example.com/api/v4/code_suggestions/completions",
]);
const body = (await result.response.json()) as any;
assert.equal(body.model, "code-gecko");
assert.match(body.choices[0].message.content, /monolith fallback works/i);
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -0,0 +1,57 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
// #10293 — anti-fold regression guard.
//
// The reported defect: Turbopack constant-folds module-load `process.platform` to the
// BUILD machine's value (a non-Windows runner) and prunes every Windows branch as dead
// code, so `dist` builds ship a tailscaleTunnel where the Windows paths are unreachable.
// That cannot be reproduced in a unit test (no published `dist`, no Windows runner), so
// this guard enforces the SOURCE invariant that makes the fold impossible: platform reads
// go through the runtime call `os.platform()` (a bundler cannot fold an arbitrary function
// call), never a module-load `process.platform` constant.
//
// If a future edit re-introduces `const IS_WINDOWS = process.platform === "win32"` (or any
// module-scope direct `process.platform` read), the folded-build failure returns — this test
// turns RED.
const modulePath = fileURLToPath(new URL("../../src/lib/tailscaleTunnel.ts", import.meta.url));
const source = fs.readFileSync(modulePath, "utf8");
test("#10293: tailscaleTunnel reads platform at runtime via os.platform(), never a module-load process.platform constant", () => {
const lines = source.split("\n");
// Any module-scope (non-function) direct read of process.platform is the foldable pattern.
const foldable = lines.filter((line, idx) => {
if (/process\.platform/.test(line) && !/^\s*\/\//.test(line)) {
// allow it only inside a function body (runtime read — but prefer os.platform there too);
// a module-load constant assignment at top level with process.platform is the defect.
return line.includes("= process.platform") && idx < 60;
}
return false;
});
assert.deepEqual(
foldable,
[],
`module-load constant(s) reading process.platform reintroduced the foldable pattern: ${foldable.join(" | ")}`
);
// The runtime getter must exist and delegate to os.platform (the anti-fold call).
assert.match(source, /function getCurrentPlatform\(\):\s*NodeJS\.Platform\s*\{\s*return os\.platform\(\);?\s*\}/m);
});
test("#10293: Windows branches use runtime platform reads, so they survive any build machine", () => {
// These are the specific Windows behaviors the reporter found folded to dead code:
// (a) --socket not injected (buildTailscaleArgs), (b) where over which (resolvePathCommand),
// (c) windows-default binary fallback (resolveBinary). Each must read platform at runtime
// through os.platform()/getCurrentPlatform().
const socketBranch = /getCurrentPlatform\(\) === "win32"[\s\S]{0,80}return args/.test(source);
const whereBranch = /os\.platform\(\) === "win32" \? "where" : "which"/.test(source);
const windowsDefaultBranch = /getCurrentPlatform\(\) === "win32" && fs\.existsSync\(WINDOWS_TAILSCALE_BIN\)/.test(source);
assert.ok(socketBranch, "buildTailscaleArgs must not inject --socket on win32 (runtime platform read)");
assert.ok(whereBranch, "resolvePathCommand must select 'where' when os.platform() === 'win32'");
assert.ok(windowsDefaultBranch, "resolveBinary must reach the Windows default binary fallback via runtime platform read");
});