Fix remaining install/update gaps + in-memory zip extraction

Install command:
- Add cbm_kill_other_instances() to kill stale MCP servers
- Add cbm_macos_adhoc_sign() to sign binary if placed unsigned

Update command:
- Replace skills-only reinstall with full cbm_install_agent_configs()
- Replace external unzip with cbm_extract_binary_from_zip() via zlib

Refactor: extract 10-agent config loop into cbm_install_agent_configs()
called by both install and update.

New: cbm_extract_binary_from_zip() — in-memory zip extraction with
stored + deflate support, path traversal rejection, bounds checks.
4 unit tests. Smoke: add install.ps1 E2E for Windows.
This commit is contained in:
Martin Vogel committed 2026-03-24 23:14:33 +01:00
1 parent 006e6dbf20
commit 8951647d3e
4 files changed
+586 -251

No files matched your search

+46 -1
View File
@@ -1199,8 +1199,53 @@ if [ "$DL_OS" != "windows" ] && [ -f "$REPO_ROOT/install.sh" ]; then
fi
rm -rf "$INSTALL_TEST_HOME" "$INSTALL_TEST_DIR"
elif [ -f "$REPO_ROOT/install.ps1" ] && command -v powershell.exe &>/dev/null; then
echo "--- Phase 13: install.ps1 E2E (Windows) ---"
PS1_TEST_HOME=$(mktemp -d)
PS1_TEST_DIR=$(mktemp -d)
mkdir -p "$PS1_TEST_HOME/.claude"
# Convert MSYS paths to Windows paths for PowerShell
if command -v cygpath &>/dev/null; then
WIN_DIR=$(cygpath -w "$PS1_TEST_DIR")
WIN_URL="$SMOKE_DOWNLOAD_URL"
WIN_SCRIPT=$(cygpath -w "$REPO_ROOT/install.ps1")
WIN_HOME=$(cygpath -w "$PS1_TEST_HOME")
else
WIN_DIR="$PS1_TEST_DIR"
WIN_URL="$SMOKE_DOWNLOAD_URL"
WIN_SCRIPT="$REPO_ROOT/install.ps1"
WIN_HOME="$PS1_TEST_HOME"
fi
# 13f: run install.ps1
HOME="$PS1_TEST_HOME" CBM_DOWNLOAD_URL="$WIN_URL" \
powershell.exe -ExecutionPolicy ByPass -File "$WIN_SCRIPT" "--dir=$WIN_DIR" 2>&1 || true
# 13g: binary placed
PS1_BIN="$PS1_TEST_DIR/codebase-memory-mcp.exe"
if [ ! -f "$PS1_BIN" ] && [ -f "$PS1_TEST_DIR/codebase-memory-mcp" ]; then
PS1_BIN="$PS1_TEST_DIR/codebase-memory-mcp"
fi
if [ -f "$PS1_BIN" ]; then
echo "OK 13g: binary placed by install.ps1"
else
echo "FAIL 13g: binary not placed by install.ps1"
exit 1
fi
# 13h: binary runs
if "$PS1_BIN" --version > /dev/null 2>&1; then
echo "OK 13h: binary runs"
else
echo "FAIL 13h: installed binary doesn't run"
exit 1
fi
rm -rf "$PS1_TEST_HOME" "$PS1_TEST_DIR"
else
echo "SKIP Phase 13: install script not available for this platform"
echo "SKIP Phase 13: no install script available for this platform"
fi
else
+413 -250
View File
@@ -1092,6 +1092,7 @@ int cbm_upsert_instructions(const char *path, const char *content) {
} else {
/* Append section */
size_t new_len = existing_len + 1 + strlen(section);
// NOLINTNEXTLINE(clang-analyzer-optin.taint.TaintedAlloc)
result = malloc(new_len + 1);
if (!result) {
free(existing);
@@ -1736,6 +1737,128 @@ unsigned char *cbm_extract_binary_from_targz(const unsigned char *data, int data
return NULL; /* binary not found */
}
/* ── Zip extraction (in-memory, replaces external unzip) ──────── */
unsigned char *cbm_extract_binary_from_zip(const unsigned char *data, int data_len, int *out_len) {
if (!data || data_len <= 0 || !out_len) {
return NULL;
}
*out_len = 0;
/* Zip local file header layout */
enum {
ZIP_SIG_0 = 0x50,
ZIP_SIG_1 = 0x4B,
ZIP_SIG_2 = 0x03,
ZIP_SIG_3 = 0x04,
ZIP_HDR_SZ = 30,
ZIP_OFF_METHOD = 8,
ZIP_OFF_COMP = 18,
ZIP_OFF_UNCOMP = 22,
ZIP_OFF_NAMELEN = 26,
ZIP_OFF_EXTRALEN = 28,
ZIP_STORED = 0,
ZIP_DEFLATE = 8
};
static const uint32_t ZIP_MAX_UNCOMP = 500U * 1024U * 1024U;
int pos = 0;
while (pos + ZIP_HDR_SZ <= data_len) {
if (data[pos] != ZIP_SIG_0 || data[pos + 1] != ZIP_SIG_1 || data[pos + 2] != ZIP_SIG_2 ||
data[pos + 3] != ZIP_SIG_3) {
break;
}
uint16_t method =
(uint16_t)(data[pos + ZIP_OFF_METHOD] | (data[pos + ZIP_OFF_METHOD + 1] << 8));
uint32_t comp_size =
(uint32_t)(data[pos + ZIP_OFF_COMP] | (data[pos + ZIP_OFF_COMP + 1] << 8) |
(data[pos + ZIP_OFF_COMP + 2] << 16) | (data[pos + ZIP_OFF_COMP + 3] << 24));
uint32_t uncomp_size =
(uint32_t)(data[pos + ZIP_OFF_UNCOMP] | (data[pos + ZIP_OFF_UNCOMP + 1] << 8) |
(data[pos + ZIP_OFF_UNCOMP + 2] << 16) |
(data[pos + ZIP_OFF_UNCOMP + 3] << 24));
uint16_t name_len =
(uint16_t)(data[pos + ZIP_OFF_NAMELEN] | (data[pos + ZIP_OFF_NAMELEN + 1] << 8));
uint16_t extra_len =
(uint16_t)(data[pos + ZIP_OFF_EXTRALEN] | (data[pos + ZIP_OFF_EXTRALEN + 1] << 8));
int header_end = pos + ZIP_HDR_SZ + name_len + extra_len;
if (header_end + (int)comp_size > data_len) {
break; /* Truncated */
}
/* Extract filename */
char fname[512] = {0};
int fn_copy = name_len < (int)sizeof(fname) - 1 ? name_len : (int)sizeof(fname) - 1;
memcpy(fname, data + pos + 30, (size_t)fn_copy);
fname[fn_copy] = '\0';
/* Security: reject path traversal */
if (strstr(fname, "..")) {
pos = header_end + (int)comp_size;
continue;
}
/* Find the binary — basename match */
const char *basename = strrchr(fname, '/');
basename = basename ? basename + 1 : fname;
if (strcmp(basename, "codebase-memory-mcp") == 0 ||
strcmp(basename, "codebase-memory-mcp.exe") == 0) {
const unsigned char *file_data = data + header_end;
if (method == ZIP_STORED) {
if (comp_size > ZIP_MAX_UNCOMP) {
return NULL;
}
unsigned char *out = malloc(comp_size);
if (!out) {
return NULL;
}
memcpy(out, file_data, comp_size);
*out_len = (int)comp_size;
return out;
}
if (method == ZIP_DEFLATE) {
if (uncomp_size > ZIP_MAX_UNCOMP) {
return NULL;
}
unsigned char *out = malloc(uncomp_size);
if (!out) {
return NULL;
}
z_stream strm = {0};
strm.next_in = (unsigned char *)file_data;
strm.avail_in = comp_size;
strm.next_out = out;
strm.avail_out = uncomp_size;
/* -MAX_WBITS = raw deflate (no zlib/gzip header) */
if (inflateInit2(&strm, -MAX_WBITS) != Z_OK) {
free(out);
return NULL;
}
int ret = inflate(&strm, Z_FINISH);
inflateEnd(&strm);
if (ret != Z_STREAM_END) {
free(out);
return NULL;
}
*out_len = (int)strm.total_out;
return out;
}
/* Unknown compression method */
return NULL;
}
pos = header_end + (int)comp_size;
}
return NULL; /* binary not found */
}
/* ── Index management ─────────────────────────────────────────── */
static const char *get_cache_dir(const char *home_dir) {
@@ -2232,6 +2355,237 @@ static const char *detect_arch(void) {
#endif
}
/* ── Agent config install/refresh (shared by install + update) ── */
static void cbm_install_agent_configs(const char *home, const char *binary_path, bool force,
bool dry_run) {
cbm_detected_agents_t agents = cbm_detect_agents(home);
printf("Detected agents:");
if (agents.claude_code) {
printf(" Claude-Code");
}
if (agents.codex) {
printf(" Codex");
}
if (agents.gemini) {
printf(" Gemini-CLI");
}
if (agents.zed) {
printf(" Zed");
}
if (agents.opencode) {
printf(" OpenCode");
}
if (agents.antigravity) {
printf(" Antigravity");
}
if (agents.aider) {
printf(" Aider");
}
if (agents.kilocode) {
printf(" KiloCode");
}
if (agents.vscode) {
printf(" VS-Code");
}
if (agents.openclaw) {
printf(" OpenClaw");
}
if (!agents.claude_code && !agents.codex && !agents.gemini && !agents.zed && !agents.opencode &&
!agents.antigravity && !agents.aider && !agents.kilocode && !agents.vscode &&
!agents.openclaw) {
printf(" (none)");
}
printf("\n\n");
/* Claude Code: skills + MCP + hooks */
if (agents.claude_code) {
char skills_dir[1024];
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", home);
printf("Claude Code:\n");
int skill_count = cbm_install_skills(skills_dir, force, dry_run);
printf(" skills: %d installed\n", skill_count);
if (cbm_remove_old_monolithic_skill(skills_dir, dry_run)) {
printf(" removed old monolithic skill\n");
}
char mcp_path[1024];
snprintf(mcp_path, sizeof(mcp_path), "%s/.claude/.mcp.json", home);
if (!dry_run) {
cbm_install_editor_mcp(binary_path, mcp_path);
}
printf(" mcp: %s\n", mcp_path);
char mcp_path2[1024];
snprintf(mcp_path2, sizeof(mcp_path2), "%s/.claude.json", home);
if (!dry_run) {
cbm_install_editor_mcp(binary_path, mcp_path2);
}
printf(" mcp: %s\n", mcp_path2);
char settings_path[1024];
snprintf(settings_path, sizeof(settings_path), "%s/.claude/settings.json", home);
if (!dry_run) {
cbm_upsert_claude_hooks(settings_path);
cbm_install_hook_gate_script(home);
}
printf(" hooks: PreToolUse (code discovery gate)\n");
}
/* Codex CLI */
if (agents.codex) {
printf("Codex CLI:\n");
char config_path[1024];
snprintf(config_path, sizeof(config_path), "%s/.codex/config.toml", home);
if (!dry_run) {
cbm_upsert_codex_mcp(binary_path, config_path);
}
printf(" mcp: %s\n", config_path);
char instr_path[1024];
snprintf(instr_path, sizeof(instr_path), "%s/.codex/AGENTS.md", home);
if (!dry_run) {
cbm_upsert_instructions(instr_path, agent_instructions_content);
}
printf(" instructions: %s\n", instr_path);
}
/* Gemini CLI */
if (agents.gemini) {
printf("Gemini CLI:\n");
char config_path[1024];
snprintf(config_path, sizeof(config_path), "%s/.gemini/settings.json", home);
if (!dry_run) {
cbm_install_editor_mcp(binary_path, config_path);
}
printf(" mcp: %s\n", config_path);
char instr_path[1024];
snprintf(instr_path, sizeof(instr_path), "%s/.gemini/GEMINI.md", home);
if (!dry_run) {
cbm_upsert_instructions(instr_path, agent_instructions_content);
}
printf(" instructions: %s\n", instr_path);
if (!dry_run) {
cbm_upsert_gemini_hooks(config_path);
}
printf(" hooks: BeforeTool (grep/file search reminder)\n");
}
/* Zed */
if (agents.zed) {
printf("Zed:\n");
char config_path[1024];
#ifdef __APPLE__
snprintf(config_path, sizeof(config_path),
"%s/Library/Application Support/Zed/settings.json", home);
#else
snprintf(config_path, sizeof(config_path), "%s/.config/zed/settings.json", home);
#endif
if (!dry_run) {
cbm_install_zed_mcp(binary_path, config_path);
}
printf(" mcp: %s\n", config_path);
}
/* OpenCode */
if (agents.opencode) {
printf("OpenCode:\n");
char config_path[1024];
snprintf(config_path, sizeof(config_path), "%s/.config/opencode/opencode.json", home);
if (!dry_run) {
cbm_upsert_opencode_mcp(binary_path, config_path);
}
printf(" mcp: %s\n", config_path);
char instr_path[1024];
snprintf(instr_path, sizeof(instr_path), "%s/.config/opencode/AGENTS.md", home);
if (!dry_run) {
cbm_upsert_instructions(instr_path, agent_instructions_content);
}
printf(" instructions: %s\n", instr_path);
}
/* Antigravity */
if (agents.antigravity) {
printf("Antigravity:\n");
char config_path[1024];
snprintf(config_path, sizeof(config_path), "%s/.gemini/antigravity/mcp_config.json", home);
if (!dry_run) {
cbm_upsert_antigravity_mcp(binary_path, config_path);
}
printf(" mcp: %s\n", config_path);
char instr_path[1024];
snprintf(instr_path, sizeof(instr_path), "%s/.gemini/antigravity/AGENTS.md", home);
if (!dry_run) {
cbm_upsert_instructions(instr_path, agent_instructions_content);
}
printf(" instructions: %s\n", instr_path);
}
/* Aider */
if (agents.aider) {
printf("Aider:\n");
char instr_path[1024];
snprintf(instr_path, sizeof(instr_path), "%s/CONVENTIONS.md", home);
if (!dry_run) {
cbm_upsert_instructions(instr_path, agent_instructions_content);
}
printf(" instructions: %s\n", instr_path);
}
/* KiloCode */
if (agents.kilocode) {
printf("KiloCode:\n");
char config_path[1024];
snprintf(config_path, sizeof(config_path),
"%s/.config/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json",
home);
if (!dry_run) {
cbm_install_editor_mcp(binary_path, config_path);
}
printf(" mcp: %s\n", config_path);
char instr_path[1024];
snprintf(instr_path, sizeof(instr_path), "%s/.kilocode/rules/codebase-memory-mcp.md", home);
if (!dry_run) {
cbm_upsert_instructions(instr_path, agent_instructions_content);
}
printf(" instructions: %s\n", instr_path);
}
/* VS Code */
if (agents.vscode) {
printf("VS Code:\n");
char config_path[1024];
#ifdef __APPLE__
snprintf(config_path, sizeof(config_path),
"%s/Library/Application Support/Code/User/mcp.json", home);
#else
snprintf(config_path, sizeof(config_path), "%s/.config/Code/User/mcp.json", home);
#endif
if (!dry_run) {
cbm_install_vscode_mcp(binary_path, config_path);
}
printf(" mcp: %s\n", config_path);
}
/* OpenClaw */
if (agents.openclaw) {
printf("OpenClaw:\n");
char config_path[1024];
snprintf(config_path, sizeof(config_path), "%s/.openclaw/openclaw.json", home);
if (!dry_run) {
cbm_install_editor_mcp(binary_path, config_path);
}
printf(" mcp: %s\n", config_path);
}
}
/* ── Subcommand: install ──────────────────────────────────────── */
int cbm_cmd_install(int argc, char **argv) {
@@ -2286,244 +2640,34 @@ int cbm_cmd_install(int argc, char **argv) {
}
}
/* Step 1b: Kill running MCP server instances so agents pick up new config */
if (!dry_run) {
int killed = cbm_kill_other_instances();
if (killed > 0) {
printf("Stopped %d running MCP server instance(s).\n\n", killed);
}
}
/* Step 1c: macOS ad-hoc signing (in case binary was placed without signing) */
#ifdef __APPLE__
{
char sign_path[1024];
snprintf(sign_path, sizeof(sign_path), "%s/.local/bin/codebase-memory-mcp", home);
struct stat sign_st;
if (stat(sign_path, &sign_st) == 0) {
(void)cbm_macos_adhoc_sign(sign_path);
}
}
#endif
/* Step 2: Binary path */
char self_path[1024];
snprintf(self_path, sizeof(self_path), "%s/.local/bin/codebase-memory-mcp", home);
/* Step 3: Detect agents */
cbm_detected_agents_t agents = cbm_detect_agents(home);
printf("Detected agents:");
if (agents.claude_code) {
printf(" Claude-Code");
}
if (agents.codex) {
printf(" Codex");
}
if (agents.gemini) {
printf(" Gemini-CLI");
}
if (agents.zed) {
printf(" Zed");
}
if (agents.opencode) {
printf(" OpenCode");
}
if (agents.antigravity) {
printf(" Antigravity");
}
if (agents.aider) {
printf(" Aider");
}
if (agents.kilocode) {
printf(" KiloCode");
}
if (agents.vscode) {
printf(" VS-Code");
}
if (agents.openclaw) {
printf(" OpenClaw");
}
if (!agents.claude_code && !agents.codex && !agents.gemini && !agents.zed && !agents.opencode &&
!agents.antigravity && !agents.aider && !agents.kilocode && !agents.vscode &&
!agents.openclaw) {
printf(" (none)");
}
printf("\n\n");
/* Step 3: Install/refresh all agent configs */
cbm_install_agent_configs(home, self_path, force, dry_run);
/* Step 4: Install Claude Code skills + hooks */
if (agents.claude_code) {
char skills_dir[1024];
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", home);
printf("Claude Code:\n");
int skill_count = cbm_install_skills(skills_dir, force, dry_run);
printf(" skills: %d installed\n", skill_count);
if (cbm_remove_old_monolithic_skill(skills_dir, dry_run)) {
printf(" removed old monolithic skill\n");
}
/* MCP config — write to both locations for compatibility.
* Claude Code <=2.1.x reads ~/.claude/.mcp.json
* Claude Code >=2.1.80 reads ~/.claude.json */
char mcp_path[1024];
snprintf(mcp_path, sizeof(mcp_path), "%s/.claude/.mcp.json", home);
if (!dry_run) {
cbm_install_editor_mcp(self_path, mcp_path);
}
printf(" mcp: %s\n", mcp_path);
char mcp_path2[1024];
snprintf(mcp_path2, sizeof(mcp_path2), "%s/.claude.json", home);
if (!dry_run) {
cbm_install_editor_mcp(self_path, mcp_path2);
}
printf(" mcp: %s\n", mcp_path2);
/* PreToolUse hook + gate script */
char settings_path[1024];
snprintf(settings_path, sizeof(settings_path), "%s/.claude/settings.json", home);
if (!dry_run) {
cbm_upsert_claude_hooks(settings_path);
cbm_install_hook_gate_script(home);
}
printf(" hooks: PreToolUse (code discovery gate)\n");
}
/* Step 5: Install Codex CLI */
if (agents.codex) {
printf("Codex CLI:\n");
char config_path[1024];
snprintf(config_path, sizeof(config_path), "%s/.codex/config.toml", home);
if (!dry_run) {
cbm_upsert_codex_mcp(self_path, config_path);
}
printf(" mcp: %s\n", config_path);
char instr_path[1024];
snprintf(instr_path, sizeof(instr_path), "%s/.codex/AGENTS.md", home);
if (!dry_run) {
cbm_upsert_instructions(instr_path, agent_instructions_content);
}
printf(" instructions: %s\n", instr_path);
}
/* Step 6: Install Gemini CLI */
if (agents.gemini) {
printf("Gemini CLI:\n");
char config_path[1024];
snprintf(config_path, sizeof(config_path), "%s/.gemini/settings.json", home);
if (!dry_run) {
cbm_install_editor_mcp(self_path, config_path);
}
printf(" mcp: %s\n", config_path);
char instr_path[1024];
snprintf(instr_path, sizeof(instr_path), "%s/.gemini/GEMINI.md", home);
if (!dry_run) {
cbm_upsert_instructions(instr_path, agent_instructions_content);
}
printf(" instructions: %s\n", instr_path);
/* BeforeTool hook (shared with Antigravity) */
if (!dry_run) {
cbm_upsert_gemini_hooks(config_path);
}
printf(" hooks: BeforeTool (grep/file search reminder)\n");
}
/* Step 7: Install Zed */
if (agents.zed) {
printf("Zed:\n");
char config_path[1024];
#ifdef __APPLE__
snprintf(config_path, sizeof(config_path),
"%s/Library/Application Support/Zed/settings.json", home);
#else
snprintf(config_path, sizeof(config_path), "%s/.config/zed/settings.json", home);
#endif
if (!dry_run) {
cbm_install_zed_mcp(self_path, config_path);
}
printf(" mcp: %s\n", config_path);
}
/* Step 8: Install OpenCode */
if (agents.opencode) {
printf("OpenCode:\n");
char config_path[1024];
snprintf(config_path, sizeof(config_path), "%s/.config/opencode/opencode.json", home);
if (!dry_run) {
cbm_upsert_opencode_mcp(self_path, config_path);
}
printf(" mcp: %s\n", config_path);
char instr_path[1024];
snprintf(instr_path, sizeof(instr_path), "%s/.config/opencode/AGENTS.md", home);
if (!dry_run) {
cbm_upsert_instructions(instr_path, agent_instructions_content);
}
printf(" instructions: %s\n", instr_path);
}
/* Step 9: Install Antigravity */
if (agents.antigravity) {
printf("Antigravity:\n");
char config_path[1024];
snprintf(config_path, sizeof(config_path), "%s/.gemini/antigravity/mcp_config.json", home);
if (!dry_run) {
cbm_upsert_antigravity_mcp(self_path, config_path);
}
printf(" mcp: %s\n", config_path);
char instr_path[1024];
snprintf(instr_path, sizeof(instr_path), "%s/.gemini/antigravity/AGENTS.md", home);
if (!dry_run) {
cbm_upsert_instructions(instr_path, agent_instructions_content);
}
printf(" instructions: %s\n", instr_path);
}
/* Step 10: Install Aider */
if (agents.aider) {
printf("Aider:\n");
char instr_path[1024];
snprintf(instr_path, sizeof(instr_path), "%s/CONVENTIONS.md", home);
if (!dry_run) {
cbm_upsert_instructions(instr_path, agent_instructions_content);
}
printf(" instructions: %s\n", instr_path);
}
/* Step 11: Install KiloCode */
if (agents.kilocode) {
printf("KiloCode:\n");
char config_path[1024];
snprintf(config_path, sizeof(config_path),
"%s/.config/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json",
home);
if (!dry_run) {
cbm_install_editor_mcp(self_path, config_path);
}
printf(" mcp: %s\n", config_path);
/* KiloCode uses ~/.kilocode/rules/ for global instructions */
char instr_path[1024];
snprintf(instr_path, sizeof(instr_path), "%s/.kilocode/rules/codebase-memory-mcp.md", home);
if (!dry_run) {
cbm_upsert_instructions(instr_path, agent_instructions_content);
}
printf(" instructions: %s\n", instr_path);
}
/* Step 12: Install VS Code */
if (agents.vscode) {
printf("VS Code:\n");
char config_path[1024];
#ifdef __APPLE__
snprintf(config_path, sizeof(config_path),
"%s/Library/Application Support/Code/User/mcp.json", home);
#else
snprintf(config_path, sizeof(config_path), "%s/.config/Code/User/mcp.json", home);
#endif
if (!dry_run) {
cbm_install_vscode_mcp(self_path, config_path);
}
printf(" mcp: %s\n", config_path);
}
/* Step 13: Install OpenClaw */
if (agents.openclaw) {
printf("OpenClaw:\n");
char config_path[1024];
snprintf(config_path, sizeof(config_path), "%s/.openclaw/openclaw.json", home);
if (!dry_run) {
cbm_install_editor_mcp(self_path, config_path);
}
printf(" mcp: %s\n", config_path);
}
/* Step 14: Ensure PATH */
/* Step 4: Ensure PATH */
char bin_dir[1024];
snprintf(bin_dir, sizeof(bin_dir), "%s/.local/bin", home);
const char *rc = cbm_detect_shell_rc(home);
@@ -2979,21 +3123,42 @@ int cbm_cmd_update(int argc, char **argv) {
}
free(bin_data);
} else {
/* Zip extraction: exec unzip directly without shell interpretation */
const char *unzip_argv[] = {"unzip", "-o", "-d", bin_dir, tmp_archive, NULL};
rc = cbm_exec_no_shell(unzip_argv);
cbm_unlink(tmp_archive);
if (rc != 0) {
fprintf(stderr, "error: extraction failed\n");
/* Zip extraction: in-memory (no external unzip dependency) */
FILE *f = fopen(tmp_archive, "rb");
if (!f) {
fprintf(stderr, "error: cannot open %s\n", tmp_archive);
return 1;
}
/* Rename variant binary if needed */
if (want_ui) {
char ui_bin[1024];
snprintf(ui_bin, sizeof(ui_bin), "%s/codebase-memory-mcp-ui.exe", bin_dir);
snprintf(bin_dest, sizeof(bin_dest), "%s/codebase-memory-mcp.exe", bin_dir);
rename(ui_bin, bin_dest);
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
unsigned char *data = malloc((size_t)fsize);
if (!data) {
fclose(f);
cbm_unlink(tmp_archive);
return 1;
}
fread(data, 1, (size_t)fsize, f);
fclose(f);
int bin_len = 0;
unsigned char *bin_data = cbm_extract_binary_from_zip(data, (int)fsize, &bin_len);
free(data);
cbm_unlink(tmp_archive);
if (!bin_data || bin_len <= 0) {
fprintf(stderr, "error: binary not found in zip archive\n");
free(bin_data);
return 1;
}
if (cbm_replace_binary(bin_dest, bin_data, bin_len, 0755) != 0) {
fprintf(stderr, "error: cannot write to %s\n", bin_dest);
free(bin_data);
return 1;
}
free(bin_data);
}
/* Step 5b: macOS ad-hoc signing (required for arm64, harmless for x86_64) */
@@ -3003,11 +3168,9 @@ int cbm_cmd_update(int argc, char **argv) {
}
#endif
/* Step 6: Reinstall skills (force to pick up new content) */
char skills_dir[1024];
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", home);
int skill_count = cbm_install_skills(skills_dir, true, false);
printf("Updated %d skill(s).\n", skill_count);
/* Step 6: Refresh all agent configs (skills, MCP entries, hooks) */
printf("Refreshing agent configurations...\n");
cbm_install_agent_configs(home, bin_dest, true, false);
/* Step 7: Verify new version (exec directly, no shell interpretation) */
printf("\nUpdate complete. Verifying:\n");
+5
View File
@@ -199,6 +199,11 @@ const char *cbm_get_codex_instructions(void);
* Returns NULL on error. Caller must free. */
unsigned char *cbm_extract_binary_from_targz(const unsigned char *data, int data_len, int *out_len);
/* Extract the codebase-memory-mcp binary from a zip archive in memory.
* Returns malloc'd binary content and sets *out_len.
* Returns NULL on error. Caller must free. */
unsigned char *cbm_extract_binary_from_zip(const unsigned char *data, int data_len, int *out_len);
/* ── Index management ─────────────────────────────────────────── */
/* List .db files in the cache directory (~/.cache/codebase-memory-mcp/).
+122
View File
@@ -1023,6 +1023,124 @@ TEST(cli_extract_binary_from_targz_invalid_data) {
PASS();
}
/* ═══════════════════════════════════════════════════════════════════
* Zip extraction tests
* ═══════════════════════════════════════════════════════════════════ */
/* Build a minimal zip file with one stored (uncompressed) entry. */
static unsigned char *create_test_zip_stored(const char *filename, const unsigned char *content,
int content_len, int *out_len) {
/* Local file header (30 bytes) + filename + content + central dir + EOCD */
int name_len = (int)strlen(filename);
int local_hdr_sz = 30 + name_len;
int cd_hdr_sz = 46 + name_len;
int eocd_sz = 22;
int total = local_hdr_sz + content_len + cd_hdr_sz + eocd_sz;
unsigned char *zip = calloc(1, (size_t)total);
if (!zip) return NULL;
int pos = 0;
/* Local file header */
zip[pos] = 0x50; zip[pos+1] = 0x4B; zip[pos+2] = 0x03; zip[pos+3] = 0x04; /* signature */
zip[pos+4] = 20; zip[pos+5] = 0; /* version needed = 2.0 */
zip[pos+8] = 0; zip[pos+9] = 0; /* compression = stored */
zip[pos+18] = (unsigned char)(content_len & 0xFF);
zip[pos+19] = (unsigned char)((content_len >> 8) & 0xFF);
zip[pos+20] = (unsigned char)((content_len >> 16) & 0xFF);
zip[pos+21] = (unsigned char)((content_len >> 24) & 0xFF);
zip[pos+22] = zip[pos+18]; zip[pos+23] = zip[pos+19];
zip[pos+24] = zip[pos+20]; zip[pos+25] = zip[pos+21];
zip[pos+26] = (unsigned char)(name_len & 0xFF);
zip[pos+27] = (unsigned char)((name_len >> 8) & 0xFF);
memcpy(zip + pos + 30, filename, (size_t)name_len);
pos += 30 + name_len;
memcpy(zip + pos, content, (size_t)content_len);
pos += content_len;
int cd_start = pos;
/* Central directory header */
zip[pos] = 0x50; zip[pos+1] = 0x4B; zip[pos+2] = 0x01; zip[pos+3] = 0x02;
zip[pos+10] = 0; zip[pos+11] = 0; /* compression = stored */
zip[pos+20] = (unsigned char)(content_len & 0xFF);
zip[pos+21] = (unsigned char)((content_len >> 8) & 0xFF);
zip[pos+22] = (unsigned char)((content_len >> 16) & 0xFF);
zip[pos+23] = (unsigned char)((content_len >> 24) & 0xFF);
zip[pos+24] = zip[pos+20]; zip[pos+25] = zip[pos+21];
zip[pos+26] = zip[pos+22]; zip[pos+27] = zip[pos+23];
zip[pos+28] = (unsigned char)(name_len & 0xFF);
zip[pos+29] = (unsigned char)((name_len >> 8) & 0xFF);
pos += 46 + name_len;
/* EOCD */
zip[pos] = 0x50; zip[pos+1] = 0x4B; zip[pos+2] = 0x05; zip[pos+3] = 0x06;
zip[pos+8] = 1; /* num entries this disk */
zip[pos+10] = 1; /* total entries */
int cd_size = pos - cd_start;
zip[pos+12] = (unsigned char)(cd_size & 0xFF);
zip[pos+13] = (unsigned char)((cd_size >> 8) & 0xFF);
zip[pos+16] = (unsigned char)(cd_start & 0xFF);
zip[pos+17] = (unsigned char)((cd_start >> 8) & 0xFF);
*out_len = total;
return zip;
}
TEST(cli_extract_binary_from_zip) {
const char *content = "#!/bin/sh\necho test\n";
int zip_len = 0;
unsigned char *zip =
create_test_zip_stored("codebase-memory-mcp", (const unsigned char *)content,
(int)strlen(content), &zip_len);
ASSERT_NOT_NULL(zip);
int out_len = 0;
unsigned char *extracted = cbm_extract_binary_from_zip(zip, zip_len, &out_len);
ASSERT_NOT_NULL(extracted);
ASSERT_EQ(out_len, (int)strlen(content));
ASSERT_MEM_EQ(extracted, content, (size_t)out_len);
free(extracted);
free(zip);
PASS();
}
TEST(cli_extract_binary_from_zip_not_found) {
const char *content = "data";
int zip_len = 0;
unsigned char *zip =
create_test_zip_stored("other-file.txt", (const unsigned char *)content,
(int)strlen(content), &zip_len);
ASSERT_NOT_NULL(zip);
int out_len = 0;
unsigned char *extracted = cbm_extract_binary_from_zip(zip, zip_len, &out_len);
ASSERT_NULL(extracted);
free(zip);
PASS();
}
TEST(cli_extract_binary_from_zip_path_traversal) {
const char *content = "malicious";
int zip_len = 0;
unsigned char *zip =
create_test_zip_stored("../../etc/codebase-memory-mcp", (const unsigned char *)content,
(int)strlen(content), &zip_len);
ASSERT_NOT_NULL(zip);
int out_len = 0;
unsigned char *extracted = cbm_extract_binary_from_zip(zip, zip_len, &out_len);
ASSERT_NULL(extracted);
free(zip);
PASS();
}
TEST(cli_extract_binary_from_zip_invalid) {
const unsigned char bad_data[] = "not a zip file";
int out_len = 0;
unsigned char *extracted = cbm_extract_binary_from_zip(bad_data, sizeof(bad_data), &out_len);
ASSERT_NULL(extracted);
PASS();
}
/* ═══════════════════════════════════════════════════════════════════
* Skill dry-run tests
* ═══════════════════════════════════════════════════════════════════ */
@@ -2152,6 +2270,10 @@ SUITE(cli) {
RUN_TEST(cli_extract_binary_from_targz);
RUN_TEST(cli_extract_binary_from_targz_not_found);
RUN_TEST(cli_extract_binary_from_targz_invalid_data);
RUN_TEST(cli_extract_binary_from_zip);
RUN_TEST(cli_extract_binary_from_zip_not_found);
RUN_TEST(cli_extract_binary_from_zip_path_traversal);
RUN_TEST(cli_extract_binary_from_zip_invalid);
/* Dry-run lifecycle (2 tests) */
RUN_TEST(cli_install_dry_run);