From 54a9a30c9d05c3f0d7fdac57386b026d1d6a7e77 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 9 Nov 2026 11:52:12 -0800 Subject: [PATCH] fix(package): preserve LF in the POSIX MCP launcher on Windows --- .gitattributes | 2 ++ ...test_check_plugin_source_compatibility.mts | 18 +++++++---- .../scripts/launch_codex_security_mcp | 1 + sdk/typescript/scripts/check-package.mjs | 11 ++++++- .../tests-ts/package-tar-listing.test.ts | 30 +++++++++++++++++++ 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/.gitattributes b/.gitattributes index 89431e09d1..414b1f9ba9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,6 +11,8 @@ *.yaml text eol=lf *.yml text eol=lf +plugins/codex-security/scripts/launch_codex_security_mcp text eol=lf + # Bundled dependencies contain intentional whitespace in strings and comments. github-action/dist/*.cjs linguist-generated=true -whitespace diff --git a/.github/scripts/test_check_plugin_source_compatibility.mts b/.github/scripts/test_check_plugin_source_compatibility.mts index 158398027e..5ff9109875 100644 --- a/.github/scripts/test_check_plugin_source_compatibility.mts +++ b/.github/scripts/test_check_plugin_source_compatibility.mts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { spawnSync, type SpawnSyncReturns } from "node:child_process"; import { copyFileSync, + mkdirSync, mkdtempSync, readFileSync, rmSync, @@ -10,7 +11,7 @@ import { writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { afterEach, test } from "node:test"; import { fileURLToPath } from "node:url"; @@ -75,19 +76,24 @@ test("accepts valid source and ignores untracked files", () => { assert.equal(result.stderr, ""); }); -test("checkout preserves source size with autocrlf", () => { +test("checkout preserves LF source and launcher bytes with autocrlf", () => { const root = fixture(); copyFileSync( new URL("../../.gitattributes", import.meta.url), join(root, ".gitattributes"), ); git(root, "config", "--local", "core.autocrlf", "true"); - for (const [name, line] of [ - ["module.py", "pass\n"], - ["module.mts", "null\n"], + for (const [name, contents] of [ + ["module.py", "pass\n".repeat(30_000)], + ["module.mts", "null\n".repeat(30_000)], + [ + "plugins/codex-security/scripts/launch_codex_security_mcp", + "#!/bin/sh\nexit 0\n", + ], ] as const) { const source = join(root, name); - const content = Buffer.from(line.repeat(30_000)); + const content = Buffer.from(contents); + mkdirSync(dirname(source), { recursive: true }); writeFileSync(source, content); git(root, "add", "--", ".gitattributes", name); unlinkSync(source); diff --git a/plugins/codex-security/scripts/launch_codex_security_mcp b/plugins/codex-security/scripts/launch_codex_security_mcp index f19251085d..c5f462d5e7 100755 --- a/plugins/codex-security/scripts/launch_codex_security_mcp +++ b/plugins/codex-security/scripts/launch_codex_security_mcp @@ -1,4 +1,5 @@ #!/bin/sh +# Use LF line endings so POSIX can execute this script directly from Windows-built packages. set -eu PATH="${PATH:-/usr/bin:/bin}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" diff --git a/sdk/typescript/scripts/check-package.mjs b/sdk/typescript/scripts/check-package.mjs index 1189a0456d..6fc6c41252 100644 --- a/sdk/typescript/scripts/check-package.mjs +++ b/sdk/typescript/scripts/check-package.mjs @@ -183,9 +183,11 @@ const listingSizes = assertTarListingSizes( ); if (rawEntries.some(({ size }, index) => size !== listingSizes[index])) invalidTarEntry(); +const posixMcpLauncher = + "package/_bundled_plugin/scripts/launch_codex_security_mcp"; for (const [path, name] of [ ["package/bin/codex-security.mjs", "CLI"], - ["package/_bundled_plugin/scripts/launch_codex_security_mcp", "MCP"], + [posixMcpLauncher, "MCP"], ]) { const permissions = listingLines[entries.indexOf(path)]?.split(/\s/u, 1)[0] ?? ""; @@ -193,6 +195,13 @@ for (const [path, name] of [ throw new Error(`npm package ${name} launcher is not executable.`); } } +if ( + [storedArchive.files, storedArchive.npmFiles].some((files) => + files.get(posixMcpLauncher)?.includes("\r\n"), + ) +) { + throw new Error("npm package POSIX MCP launcher contains CRLF line endings."); +} function privateExtractionDirectories(directory) { chmodSync(directory, 0o700); diff --git a/sdk/typescript/tests-ts/package-tar-listing.test.ts b/sdk/typescript/tests-ts/package-tar-listing.test.ts index 3b55f937f2..2a6aa1d168 100644 --- a/sdk/typescript/tests-ts/package-tar-listing.test.ts +++ b/sdk/typescript/tests-ts/package-tar-listing.test.ts @@ -328,6 +328,36 @@ describe("npm package tar listings", () => { ).toThrow("npm tarball contains a non-regular entry"); }); + test.each([ + ["shebang", "#!/bin/sh\r\nexit 0\n"], + ["body", "#!/bin/sh\nexit 0\r\n"], + ])( + "rejects CRLF in the packaged POSIX MCP launcher's %s", + async (_part, script) => { + const root = mkdtempSync(join(tmpdir(), "codex-package-launcher-")); + try { + const path = + "package/_bundled_plugin/scripts/launch_codex_security_mcp"; + const launcher = tarRecord(Buffer.from(script), { + name: path, + mode: 0o755, + }); + const archivePath = join(root, "launcher.tgz"); + writeFileSync( + archivePath, + gzipSync(packageTar({ overrides: new Map([[path, launcher]]) })), + ); + const contractPath = join(root, "plugin contract.json"); + writeFileSync(contractPath, JSON.stringify(pluginContract)); + const result = await checkPackage(root, archivePath, contractPath); + expect(result.status).toBe(1); + expect(result.stderr).toContain("POSIX MCP launcher contains CRLF"); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }, + ); + test.each([false, true])( "rejects invalid package paths before Brotli expansion, complete=%p", async (complete) => {