From 8951647d3e440ac2850a8ea3e027084e5331e1f8 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 24 Mar 2026 20:50:26 +0100 Subject: [PATCH] Fix remaining install/update gaps + in-memory zip extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- scripts/smoke-test.sh | 47 ++- src/cli/cli.c | 663 ++++++++++++++++++++++++++---------------- src/cli/cli.h | 5 + tests/test_cli.c | 122 ++++++++ 4 files changed, 586 insertions(+), 251 deletions(-) diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 8325ed9eb..51a9559e9 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -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 diff --git a/src/cli/cli.c b/src/cli/cli.c index ab880d4c1..dbee30079 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -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"); diff --git a/src/cli/cli.h b/src/cli/cli.h index 6834f6886..efa022e7a 100644 --- a/src/cli/cli.h +++ b/src/cli/cli.h @@ -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/). diff --git a/tests/test_cli.c b/tests/test_cli.c index a4b2110fc..ecf580b67 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -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);