fix: enforce executable permissions on binary entry points during pack
When server.type is "binary", mcpb pack now ensures the entry_point file has execute bits (|0o111) in the ZIP external_attr, regardless of the source filesystem permissions. This prevents EACCES failures when Claude Desktop extracts Windows-built binary MCPBs (which store 644 because Windows has no Unix execute bit). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f14863bcdc
commit
122ef0e9e9
2 changed files with 88 additions and 4 deletions
|
|
@ -206,15 +206,22 @@ export async function packExtension({
|
|||
const zipFiles: Zippable = {};
|
||||
|
||||
const isUnix = process.platform !== "win32";
|
||||
const isBinaryType = manifest.server?.type === "binary";
|
||||
const binaryEntryPoint = isBinaryType ? manifest.server.entry_point : null;
|
||||
|
||||
for (const [filePath, fileData] of Object.entries(files)) {
|
||||
if (isUnix) {
|
||||
let mode = fileData.mode & 0o777;
|
||||
// Ensure binary entry points are always executable in the ZIP,
|
||||
// regardless of filesystem permissions. Windows-built ZIPs store
|
||||
// 644 because Windows has no Unix execute bit — CD spawns binary
|
||||
// entry points directly, so they must have +x to avoid EACCES.
|
||||
if (binaryEntryPoint && filePath === binaryEntryPoint) {
|
||||
mode = mode | 0o111;
|
||||
}
|
||||
// Set external file attributes to preserve Unix permissions
|
||||
// The mode needs to be shifted to the upper 16 bits for ZIP format
|
||||
zipFiles[filePath] = [
|
||||
fileData.data,
|
||||
{ os: 3, attrs: (fileData.mode & 0o777) << 16 },
|
||||
];
|
||||
zipFiles[filePath] = [fileData.data, { os: 3, attrs: mode << 16 }];
|
||||
} else {
|
||||
// On Windows, use default ZIP attributes (no Unix permissions)
|
||||
zipFiles[filePath] = fileData.data;
|
||||
|
|
|
|||
|
|
@ -357,5 +357,82 @@ describe("DXT CLI", () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should add group+other execute bits on binary entry points during pack", () => {
|
||||
// Skip this test on Windows since it doesn't support Unix permissions
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
|
||||
const tempBinDir = join(__dirname, "temp-binary-entry-test");
|
||||
const binPackedFilePath = join(__dirname, "test-binary-entry.dxt");
|
||||
const binUnpackedDir = join(__dirname, "temp-binary-entry-unpack");
|
||||
|
||||
try {
|
||||
// Create a binary-type extension with owner-only execute (0o700)
|
||||
// This passes validation (has some +x) but pack should add
|
||||
// group+other execute bits for compatibility with CD extraction
|
||||
fs.mkdirSync(join(tempBinDir, "server"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
join(tempBinDir, "manifest.json"),
|
||||
JSON.stringify({
|
||||
manifest_version: DEFAULT_MANIFEST_VERSION,
|
||||
name: "Test Binary Extension",
|
||||
version: "1.0.0",
|
||||
description: "A test extension with binary entry point",
|
||||
author: {
|
||||
name: "MCPB",
|
||||
},
|
||||
server: {
|
||||
type: "binary",
|
||||
entry_point: "server/myserver",
|
||||
mcp_config: {
|
||||
command: "${__dirname}/server/myserver",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Create binary entry point with 700 (owner-only execute)
|
||||
fs.writeFileSync(
|
||||
join(tempBinDir, "server", "myserver"),
|
||||
"#!/bin/sh\necho binary",
|
||||
);
|
||||
fs.chmodSync(join(tempBinDir, "server", "myserver"), 0o700);
|
||||
|
||||
// Create a regular file for comparison (should stay 644)
|
||||
fs.writeFileSync(join(tempBinDir, "config.json"), "{}");
|
||||
fs.chmodSync(join(tempBinDir, "config.json"), 0o644);
|
||||
|
||||
// Pack the extension
|
||||
execSync(`node ${cliPath} pack ${tempBinDir} ${binPackedFilePath}`, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
// Unpack to check stored permissions
|
||||
execSync(
|
||||
`node ${cliPath} unpack ${binPackedFilePath} ${binUnpackedDir}`,
|
||||
{
|
||||
encoding: "utf-8",
|
||||
},
|
||||
);
|
||||
|
||||
// Binary entry point should have full execute bits (700 | 111 = 711)
|
||||
const entryStats = fs.statSync(
|
||||
join(binUnpackedDir, "server", "myserver"),
|
||||
);
|
||||
expect(entryStats.mode & 0o777).toBe(0o711);
|
||||
|
||||
// Non-entry-point file should retain original permissions
|
||||
const configStats = fs.statSync(join(binUnpackedDir, "config.json"));
|
||||
expect(configStats.mode & 0o777).toBe(0o644);
|
||||
} finally {
|
||||
fs.rmSync(tempBinDir, { recursive: true, force: true });
|
||||
fs.rmSync(binUnpackedDir, { recursive: true, force: true });
|
||||
if (fs.existsSync(binPackedFilePath)) {
|
||||
fs.unlinkSync(binPackedFilePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue