Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 12 additions & 6 deletions .github/scripts/test_check_plugin_source_compatibility.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import { spawnSync, type SpawnSyncReturns } from "node:child_process";
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
Expand All @@ -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";

Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions plugins/codex-security/scripts/launch_codex_security_mcp
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
11 changes: 10 additions & 1 deletion sdk/typescript/scripts/check-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -183,16 +183,25 @@ 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] ?? "";
if ([3, 6, 9].some((index) => permissions[index] !== "x")) {
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);
Expand Down
30 changes: 30 additions & 0 deletions sdk/typescript/tests-ts/package-tar-listing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading