feat: coordinate concurrent CBM sessions

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-07-16 19:20:46 +02:00
1 parent a688d85cc8
commit 0e00ef5702
112 files changed
+63161 -2288

No files matched your search

+128 -27
View File
@@ -13,22 +13,33 @@ import (
"archive/tar"
"archive/zip"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
)
const (
repo = "DeusData/codebase-memory-mcp"
version = "0.8.1"
maxRedirects = 5
requestTimeout = 2 * time.Minute
connectTimeout = 15 * time.Second
responseHeaderTimeout = 30 * time.Second
candidateTimeout = 15 * time.Second
maxChecksumManifestSize = 1024 * 1024
)
func main() {
@@ -117,7 +128,11 @@ func download(dest string) error {
ext = "zip"
}
archive := fmt.Sprintf("codebase-memory-mcp-%s-%s.%s", platform, arch, ext)
portable := ""
if platform == "linux" {
portable = "-portable"
}
archive := fmt.Sprintf("codebase-memory-mcp-%s-%s%s.%s", platform, arch, portable, ext)
url := fmt.Sprintf("https://github.com/%s/releases/download/v%s/%s", repo, version, archive)
checksumURL := fmt.Sprintf("https://github.com/%s/releases/download/v%s/checksums.txt", repo, version)
@@ -134,13 +149,18 @@ func download(dest string) error {
return fmt.Errorf("download failed: %w", err)
}
// Verify checksum if available (non-fatal if checksums.txt unreachable)
if checksums, err := fetchChecksums(checksumURL); err == nil {
if expected, ok := checksums[archive]; ok {
if err := verifyChecksum(archivePath, expected); err != nil {
return err
}
}
// A release binary is executable input, so checksum verification is a
// mandatory precondition rather than a best-effort warning.
checksums, err := fetchChecksums(checksumURL)
if err != nil {
return fmt.Errorf("checksum manifest unavailable: %w", err)
}
expected, ok := checksums[archive]
if !ok {
return fmt.Errorf("checksum manifest has no entry for %s", archive)
}
if err := verifyChecksum(archivePath, expected); err != nil {
return err
}
binName := "codebase-memory-mcp"
@@ -157,25 +177,29 @@ func download(dest string) error {
return fmt.Errorf("extraction failed: %w", err)
}
}
extracted := filepath.Join(tmp, binName)
if err := os.Chmod(extracted, 0755); err != nil {
return fmt.Errorf("could not set candidate permissions: %w", err)
}
if err := verifyCandidate(extracted); err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return fmt.Errorf("could not create cache dir: %w", err)
}
if err := copyFile(filepath.Join(tmp, binName), dest); err != nil {
if err := installCandidateAtomically(extracted, dest); err != nil {
return fmt.Errorf("could not install binary: %w", err)
}
if err := os.Chmod(dest, 0755); err != nil {
return fmt.Errorf("could not set permissions: %w", err)
}
return nil
}
// validateURLScheme rejects non-https URLs before any fetch (defense-in-depth).
func validateURLScheme(rawURL string) error {
if !strings.HasPrefix(rawURL, "https://") {
parsed, err := url.Parse(rawURL)
if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil {
return fmt.Errorf("refusing non-https URL: %s", rawURL)
}
return nil
@@ -183,11 +207,20 @@ func validateURLScheme(rawURL string) error {
// httpsOnlyClient returns an HTTP client that rejects non-HTTPS redirects.
var httpsOnlyClient = &http.Client{
Timeout: requestTimeout,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{Timeout: connectTimeout}).DialContext,
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: connectTimeout,
ResponseHeaderTimeout: responseHeaderTimeout,
ExpectContinueTimeout: time.Second,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return fmt.Errorf("refusing non-https redirect to %s", req.URL)
if err := validateURLScheme(req.URL.String()); err != nil {
return fmt.Errorf("refusing unsafe redirect: %w", err)
}
if len(via) >= 10 {
if len(via) > maxRedirects {
return fmt.Errorf("too many redirects")
}
return nil
@@ -210,9 +243,12 @@ func httpGet(rawURL, dest string) error {
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
_, copyErr := io.Copy(f, resp.Body)
closeErr := f.Close()
if copyErr != nil {
return copyErr
}
return closeErr
}
func fetchChecksums(url string) (map[string]string, error) {
@@ -227,21 +263,45 @@ func fetchChecksums(url string) (map[string]string, error) {
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, maxChecksumManifestSize+1))
if err != nil {
return nil, err
}
if len(body) > maxChecksumManifestSize {
return nil, fmt.Errorf("checksums.txt exceeds the 1 MiB safety limit")
}
result := make(map[string]string)
for _, line := range strings.Split(string(body), "\n") {
parts := strings.Fields(line)
if len(parts) == 2 {
result[parts[1]] = parts[0]
name := parts[1]
if strings.HasPrefix(name, "*") {
name = name[1:]
}
digest := strings.ToLower(parts[0])
if len(digest) != sha256.Size*2 {
return nil, fmt.Errorf("invalid SHA-256 checksum for %s", name)
}
if _, err := hex.DecodeString(digest); err != nil {
return nil, fmt.Errorf("invalid SHA-256 checksum for %s: %w", name, err)
}
if prior, exists := result[name]; exists && prior != digest {
return nil, fmt.Errorf("conflicting SHA-256 checksums for %s", name)
}
result[name] = digest
}
}
return result, nil
}
func verifyChecksum(path, expected string) error {
expected = strings.ToLower(expected)
if len(expected) != sha256.Size*2 {
return fmt.Errorf("invalid SHA-256 checksum length")
}
if _, err := hex.DecodeString(expected); err != nil {
return fmt.Errorf("invalid SHA-256 checksum: %w", err)
}
f, err := os.Open(path)
if err != nil {
return err
@@ -316,19 +376,60 @@ func extractZip(archivePath, destDir, targetFile string) error {
return fmt.Errorf("%s not found in archive", targetFile)
}
func copyFile(src, dst string) error {
func verifyCandidate(path string) error {
ctx, cancel := context.WithTimeout(context.Background(), candidateTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, path, "--version")
cmd.Stdin = nil
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
if err := cmd.Run(); err != nil {
if ctx.Err() != nil {
return fmt.Errorf("downloaded binary verification timed out: %w", ctx.Err())
}
return fmt.Errorf("downloaded binary failed to run: %w", err)
}
return nil
}
func installCandidateAtomically(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
pattern := "." + filepath.Base(dst) + ".*.tmp"
if runtime.GOOS == "windows" {
pattern += ".exe"
}
out, err := os.CreateTemp(filepath.Dir(dst), pattern)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
staged := out.Name()
defer os.Remove(staged)
if _, err := io.Copy(out, in); err != nil {
out.Close()
return err
}
if err := out.Chmod(0755); err != nil {
out.Close()
return err
}
if err := out.Sync(); err != nil {
out.Close()
return err
}
if err := out.Close(); err != nil {
return err
}
if err := verifyCandidate(staged); err != nil {
return err
}
if err := os.Rename(staged, dst); err != nil {
return err
}
return nil
}
func execBinary(bin string, args []string) error {
+171 -41
View File
@@ -9,10 +9,15 @@ const fs = require('fs');
const path = require('path');
const os = require('os');
const { execFileSync } = require('child_process');
const { pipeline } = require('stream');
const REPO = 'DeusData/codebase-memory-mcp';
const VERSION = require('./package.json').version;
const BIN_DIR = path.join(__dirname, 'bin');
const MAX_REDIRECTS = 5;
const DOWNLOAD_HOP_TIMEOUT_MS = 120_000;
const CANDIDATE_TIMEOUT_MS = 15_000;
const MAX_CHECKSUM_MANIFEST_BYTES = 1024 * 1024;
function getPlatform() {
switch (process.platform) {
@@ -31,36 +36,145 @@ function getArch() {
}
}
// Security: only follow HTTPS URLs (defense-in-depth).
function validateUrl(url) {
if (!url.startsWith('https://')) {
throw new Error(`Refusing non-HTTPS URL: ${url}`);
// Security: only follow HTTPS URLs (defense-in-depth). Parse the URL instead
// of relying on a string prefix so every redirect is checked unambiguously.
function validateUrl(rawUrl) {
let parsed;
try {
parsed = new URL(rawUrl);
} catch (err) {
throw new Error(`Invalid download URL ${rawUrl}: ${err.message}`);
}
if (parsed.protocol !== 'https:' || !parsed.hostname || parsed.username || parsed.password) {
throw new Error(`Refusing non-HTTPS or credentialed URL: ${rawUrl}`);
}
return parsed.href;
}
function downloadHop(url, dest, maxBytes) {
return new Promise((resolve, reject) => {
let settled = false;
let response = null;
let timer = null;
function finish(err, result) {
if (settled) return;
settled = true;
if (timer) clearTimeout(timer);
if (err) reject(err);
else resolve(result);
}
const req = https.get(url, (res) => {
response = res;
res.on('error', (err) => finish(err));
const redirectCodes = new Set([301, 302, 303, 307, 308]);
if (redirectCodes.has(res.statusCode)) {
const location = res.headers.location;
if (!location) {
res.destroy();
finish(new Error(`Redirect with no location for ${url}`));
return;
}
let next;
try {
next = validateUrl(new URL(location, url).href);
} catch (err) {
res.destroy();
finish(err);
return;
}
res.destroy();
finish(null, { redirect: next });
return;
}
if (res.statusCode !== 200) {
res.destroy();
finish(new Error(`HTTP ${res.statusCode} for ${url}`));
return;
}
let received = 0;
res.on('data', (chunk) => {
received += chunk.length;
if (maxBytes && received > maxBytes) {
res.destroy(new Error(`Download exceeds the ${maxBytes}-byte safety limit`));
}
});
const file = fs.createWriteStream(dest, { flags: 'w' });
pipeline(res, file, (err) => {
finish(err, { redirect: null });
});
});
req.on('error', (err) => finish(err));
timer = setTimeout(() => {
const err = new Error(`Download hop timed out after ${DOWNLOAD_HOP_TIMEOUT_MS} ms: ${url}`);
req.destroy(err);
if (response) response.destroy(err);
}, DOWNLOAD_HOP_TIMEOUT_MS);
});
}
async function download(rawUrl, dest, maxBytes = 0) {
let url = validateUrl(rawUrl);
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects += 1) {
const result = await downloadHop(url, dest, maxBytes);
if (!result.redirect) return;
if (redirects === MAX_REDIRECTS) throw new Error('Too many redirects');
url = result.redirect;
}
}
function download(url, dest) {
validateUrl(url);
return new Promise((resolve, reject) => {
function follow(u, depth) {
if (depth > 5) return reject(new Error('Too many redirects'));
validateUrl(u);
https.get(u, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
const loc = res.headers.location;
if (!loc) return reject(new Error('Redirect with no location'));
const next = loc.startsWith('/') ? new URL(loc, u).href : loc;
return follow(next, depth + 1);
}
if (res.statusCode !== 200) {
return reject(new Error(`HTTP ${res.statusCode} for ${u}`));
}
const file = fs.createWriteStream(dest);
res.pipe(file);
file.on('finish', () => file.close(resolve));
file.on('error', reject);
}).on('error', reject);
function parseExpectedChecksum(manifest, archiveName) {
let expected = null;
for (const line of manifest.split('\n')) {
const fields = line.trim().split(/\s+/);
if (fields.length < 2) continue;
if (fields[1] !== archiveName && fields[1] !== `*${archiveName}`) continue;
const digest = fields[0].toLowerCase();
if (!/^[0-9a-f]{64}$/.test(digest)) {
throw new Error(`Invalid SHA-256 checksum for ${archiveName}`);
}
follow(url, 0);
if (expected !== null && expected !== digest) {
throw new Error(`Conflicting SHA-256 checksums for ${archiveName}`);
}
expected = digest;
}
if (expected === null) {
throw new Error(`No checksum for ${archiveName} in checksums.txt`);
}
return expected;
}
function verifyCandidate(candidatePath) {
execFileSync(candidatePath, ['--version'], {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: CANDIDATE_TIMEOUT_MS,
windowsHide: true,
});
}
function extractZipOnWindows(archivePath, destPath) {
// -EncodedCommand is a constant program. Paths travel only through the child
// environment and are consumed with -LiteralPath, so PowerShell never parses
// user/TEMP path bytes as source code or wildcard syntax.
const script = [
"$ErrorActionPreference = 'Stop'",
'Expand-Archive -LiteralPath $env:CBM_NPM_ARCHIVE_PATH ' +
'-DestinationPath $env:CBM_NPM_DEST_PATH -Force',
].join('; ');
const encoded = Buffer.from(script, 'utf16le').toString('base64');
execFileSync('powershell', [
'-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', encoded,
], {
env: {
...process.env,
CBM_NPM_ARCHIVE_PATH: archivePath,
CBM_NPM_DEST_PATH: destPath,
},
stdio: 'inherit',
windowsHide: true,
});
}
@@ -69,11 +183,14 @@ async function verifyChecksum(archivePath, archiveName) {
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/checksums.txt`;
const tmpChecksums = archivePath + '.checksums';
try {
await download(url, tmpChecksums);
const lines = fs.readFileSync(tmpChecksums, 'utf-8').split('\n');
const match = lines.find((l) => l.includes(archiveName));
if (!match) return; // checksum line not found — non-fatal
const expected = match.split(/\s+/)[0];
await download(url, tmpChecksums, MAX_CHECKSUM_MANIFEST_BYTES);
const manifestSize = fs.statSync(tmpChecksums).size;
if (manifestSize > MAX_CHECKSUM_MANIFEST_BYTES) {
throw new Error('checksums.txt exceeds the 1 MiB safety limit');
}
const expected = parseExpectedChecksum(
fs.readFileSync(tmpChecksums, 'utf-8'), archiveName,
);
const actual = crypto
.createHash('sha256')
.update(fs.readFileSync(archivePath))
@@ -84,9 +201,6 @@ async function verifyChecksum(archivePath, archiveName) {
);
}
process.stdout.write('codebase-memory-mcp: checksum verified.\n');
} catch (err) {
if (err.message.startsWith('Checksum mismatch')) throw err;
// Non-fatal: checksum unavailable (network issue, pre-release, etc.)
} finally {
try { fs.unlinkSync(tmpChecksums); } catch (_) { /* ignore */ }
}
@@ -100,7 +214,12 @@ async function main() {
const binPath = path.join(BIN_DIR, binName);
if (fs.existsSync(binPath)) {
return; // already installed, nothing to do
try {
verifyCandidate(binPath);
return; // already installed and runnable, nothing to do
} catch (_) {
fs.rmSync(binPath, { force: true });
}
}
fs.mkdirSync(BIN_DIR, { recursive: true });
@@ -129,10 +248,7 @@ async function main() {
if (ext === 'tar.gz') {
execFileSync('tar', ['-xzf', tmpArchive, '-C', tmpDir, '--no-same-owner']);
} else {
execFileSync('powershell', [
'-NoProfile', '-Command',
`Expand-Archive -Path '${tmpArchive}' -DestinationPath '${tmpDir}' -Force`,
]);
extractZipOnWindows(tmpArchive, tmpDir);
}
// Validate extracted path doesn't escape tmpDir (tar-slip defense).
@@ -146,8 +262,22 @@ async function main() {
throw new Error(`Binary not found after extraction at ${extracted}`);
}
fs.copyFileSync(extracted, binPath);
fs.chmodSync(binPath, 0o755);
fs.chmodSync(extracted, 0o755);
verifyCandidate(extracted);
const stagedSuffix = platform === 'windows' ? '.tmp.exe' : '.tmp';
const staged = path.join(
BIN_DIR,
`.${binName}.${process.pid}.${crypto.randomBytes(8).toString('hex')}${stagedSuffix}`,
);
try {
fs.copyFileSync(extracted, staged, fs.constants.COPYFILE_EXCL);
fs.chmodSync(staged, 0o755);
verifyCandidate(staged);
fs.renameSync(staged, binPath);
} finally {
try { fs.unlinkSync(staged); } catch (_) { /* renamed or never created */ }
}
process.stdout.write('codebase-memory-mcp: ready.\n');
} finally {
+175 -36
View File
@@ -2,14 +2,16 @@
import hashlib
import os
import sys
import platform
import stat
import shutil
import stat
import subprocess
import sys
import tempfile
import urllib.request
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
REPO = "DeusData/codebase-memory-mcp"
@@ -18,18 +20,101 @@ REPO = "DeusData/codebase-memory-mcp"
# file://, ftp://, and custom schemes — a redirect or tainted URL source
# could otherwise turn a download into an arbitrary-local-file read.
_ALLOWED_SCHEMES = frozenset({"https"})
_MAX_REDIRECTS = 5
_NETWORK_TIMEOUT_SECONDS = 120
_CANDIDATE_TIMEOUT_SECONDS = 15
_MAX_CHECKSUM_MANIFEST_BYTES = 1024 * 1024
_REDIRECT_CODES = frozenset({301, 302, 303, 307, 308})
class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
"""Expose redirects to _download_https for explicit per-hop validation."""
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
_HTTPS_OPENER = urllib.request.build_opener(_NoRedirectHandler())
def _validate_url_scheme(url: str) -> None:
"""Reject non-https URLs before any network fetch."""
scheme = urllib.parse.urlparse(url).scheme
if scheme not in _ALLOWED_SCHEMES:
parsed = urllib.parse.urlparse(url)
if (
parsed.scheme not in _ALLOWED_SCHEMES
or not parsed.hostname
or parsed.username is not None
or parsed.password is not None
):
sys.exit(
f"codebase-memory-mcp: refusing to fetch non-https URL "
f"(scheme={scheme!r}): {url}"
f"codebase-memory-mcp: refusing to fetch invalid, credentialed, or "
f"non-https URL: {url}"
)
def _download_https(url: str, dest: str, max_bytes: int = 0) -> None:
"""Download through a bounded HTTPS-only opener into dest."""
current_url = url
for redirect_count in range(_MAX_REDIRECTS + 1):
_validate_url_scheme(current_url)
request = urllib.request.Request(
current_url, headers={"User-Agent": "codebase-memory-mcp-installer"}
)
try:
response = _HTTPS_OPENER.open(
request, timeout=_NETWORK_TIMEOUT_SECONDS
)
except urllib.error.HTTPError as exc:
if exc.code not in _REDIRECT_CODES:
raise
location = exc.headers.get("Location")
exc.close()
if not location:
raise RuntimeError(f"redirect has no Location: {current_url}")
if redirect_count == _MAX_REDIRECTS:
raise RuntimeError("too many redirects")
current_url = urllib.parse.urljoin(current_url, location)
_validate_url_scheme(current_url)
continue
with response:
_validate_url_scheme(response.geturl())
deadline = time.monotonic() + _NETWORK_TIMEOUT_SECONDS
total = 0
with open(dest, "wb") as out:
while True:
if time.monotonic() >= deadline:
raise TimeoutError(
f"download hop timed out: {current_url}"
)
chunk = response.read(65536)
if not chunk:
return
total += len(chunk)
if max_bytes and total > max_bytes:
raise RuntimeError(
f"download exceeds the {max_bytes}-byte safety limit"
)
out.write(chunk)
raise RuntimeError("too many redirects")
def _verify_candidate(path: Path) -> None:
"""Require a staged native candidate to execute successfully."""
try:
subprocess.run(
[str(path), "--version"],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
timeout=_CANDIDATE_TIMEOUT_SECONDS,
)
except (OSError, subprocess.SubprocessError) as exc:
raise RuntimeError(f"downloaded binary failed to run: {exc}") from exc
def _safe_extract_tar(tf, dest: str) -> None:
"""Extract a tarfile to dest, rejecting path-traversal entries.
@@ -73,37 +158,61 @@ def _safe_extract_zip(zf, dest: str) -> None:
def _verify_checksum(archive_path: str, archive_name: str, version: str) -> None:
"""Verify SHA256 checksum against checksums.txt from the release."""
url = f"https://github.com/{REPO}/releases/download/v{version}/checksums.txt"
tmp_path = None
try:
_validate_url_scheme(url)
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
tmp_path = tmp.name
urllib.request.urlretrieve(url, tmp_path) # noqa: S310 — scheme validated above
with open(tmp_path) as f:
_download_https(url, tmp_path, _MAX_CHECKSUM_MANIFEST_BYTES)
expected = None
with open(tmp_path, encoding="utf-8") as f:
for line in f:
if archive_name in line:
expected = line.split()[0]
h = hashlib.sha256()
with open(archive_path, "rb") as af:
for chunk in iter(lambda: af.read(65536), b""):
h.update(chunk)
actual = h.hexdigest()
if expected != actual:
sys.exit(
f"codebase-memory-mcp: CHECKSUM MISMATCH for {archive_name}\n"
f" expected: {expected}\n"
f" actual: {actual}"
)
print("codebase-memory-mcp: checksum verified.", file=sys.stderr)
break
fields = line.split()
if len(fields) < 2 or fields[1] not in (
archive_name,
f"*{archive_name}",
):
continue
digest = fields[0].lower()
if len(digest) != 64 or any(
ch not in "0123456789abcdef" for ch in digest
):
sys.exit(
f"codebase-memory-mcp: invalid SHA256 checksum for "
f"{archive_name}"
)
if expected is not None and expected != digest:
sys.exit(
f"codebase-memory-mcp: conflicting SHA256 checksums for "
f"{archive_name}"
)
expected = digest
if expected is None:
sys.exit(
f"codebase-memory-mcp: no checksum for {archive_name} in checksums.txt"
)
h = hashlib.sha256()
with open(archive_path, "rb") as af:
for chunk in iter(lambda: af.read(65536), b""):
h.update(chunk)
actual = h.hexdigest()
if expected != actual:
sys.exit(
f"codebase-memory-mcp: CHECKSUM MISMATCH for {archive_name}\n"
f" expected: {expected}\n"
f" actual: {actual}"
)
print("codebase-memory-mcp: checksum verified.", file=sys.stderr)
except SystemExit:
raise
except Exception:
pass # Non-fatal: checksum unavailable
except Exception as exc:
sys.exit(f"codebase-memory-mcp: checksum verification failed: {exc}")
finally:
try:
os.unlink(tmp_path)
except Exception:
pass
if tmp_path is not None:
try:
os.unlink(tmp_path)
except Exception:
pass
def _version() -> str:
@@ -175,8 +284,8 @@ def _download(version: str) -> Path:
with tempfile.TemporaryDirectory() as tmp:
tmp_archive = os.path.join(tmp, f"cbm.{ext}")
try:
urllib.request.urlretrieve(url, tmp_archive) # noqa: S310 — scheme validated above
except urllib.error.HTTPError as e:
_download_https(url, tmp_archive)
except (OSError, RuntimeError, urllib.error.URLError) as e:
sys.exit(
f"codebase-memory-mcp: download failed ({e})\n"
f"URL: {url}\n"
@@ -199,9 +308,40 @@ def _download(version: str) -> Path:
if not os.path.exists(extracted):
sys.exit("codebase-memory-mcp: binary not found after extraction")
shutil.copy2(extracted, dest)
current = dest.stat().st_mode
dest.chmod(current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
extracted_path = Path(extracted)
current = extracted_path.stat().st_mode
extracted_path.chmod(current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
try:
_verify_candidate(extracted_path)
except RuntimeError as exc:
sys.exit(f"codebase-memory-mcp: {exc}")
staged_path = None
try:
staged_suffix = ".tmp.exe" if sys.platform == "win32" else ".tmp"
with tempfile.NamedTemporaryFile(
dir=str(dest.parent),
prefix=f".{dest.name}.",
suffix=staged_suffix,
delete=False,
) as staged:
staged_path = Path(staged.name)
shutil.copy2(extracted_path, staged_path)
staged_mode = staged_path.stat().st_mode
staged_path.chmod(
staged_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
)
_verify_candidate(staged_path)
os.replace(staged_path, dest)
staged_path = None
except RuntimeError as exc:
sys.exit(f"codebase-memory-mcp: {exc}")
finally:
if staged_path is not None:
try:
staged_path.unlink()
except OSError:
pass
return dest
@@ -222,6 +362,5 @@ def main() -> None:
if sys.platform != "win32":
os.execv(str(bin_path), args) # noqa: S606 — list form, no shell
else:
import subprocess
result = subprocess.run(args) # noqa: S603 — list form, no shell=True
sys.exit(result.returncode)