fix(cli): copy and sign the built binary on install instead of re-signing the stale target#473
fix(cli): copy and sign the built binary on install instead of re-signing the stale target#473halindrome wants to merge 3 commits into
Conversation
QA Round 1 — PR #473Verdict: APPROVE WITH NITS. The fix correctly resolves #472: Findings
Detail on the items that matterSemantic equivalence with Force/prompt/
When Dry-run. Copy block prints "Would install …" and performs no mkdir/copy; the macOS sign block is gated by Windows. Required changesNone. Recommended (non-blocking): add a symlink/relative-path case to |
QA Round 2 — PR #473Verdict: APPROVE WITH NITS. The fix is correct and complete for the bug it targets (#472). End-to-end the new flow is sound: it detects the running binary (
Notes on items specifically probed
Required changes(none) |
…ning the stale target `install` (especially with --force) only ad-hoc-signed whatever already sat at ~/.local/bin/codebase-memory-mcp and never copied the binary the operator ran install from. An operator who built from source and ran `install -y --force` was silently left with the OLD binary at the target — running stale code while believing they had upgraded (DeusData#472). Install now mirrors `update`: detect the running binary, copy it to the canonical target (skipping the copy when we already ARE the target, which would otherwise truncate the running file), make it executable, ad-hoc sign the TARGET, and point all agent configs at the target. A different pre-existing binary is only replaced with --force or interactive confirmation; --dry-run reports the intended swap without touching anything. The copy+sign also resolves the secondary report: a hand-copied clang build is linker-signed (flags=0x20002) and gets Killed:9 when spawned by an MCP host — re-signing ad-hoc (flags=0x2) makes it launchable, which install now does for the placed binary. Adds cbm_copy_binary_to_target() (with a same-file guard) plus regression tests covering the binary swap and the self-copy guard. Closes DeusData#472 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Extend cli_install_same_file_guard_issue472 with a symlink case: two distinct path strings resolving to the same inode (which a non-canonical cbm_detect_self_path vs the hardcoded target can produce) must also be detected as same-file and skipped, not truncated. POSIX-only (#ifndef _WIN32). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
43641a4 to
3294bbe
Compare
cli_install_copies_binary_to_target_issue472 asserted the executable bit on the copied target, but cbm_copy_binary_to_target only chmods on POSIX (the exec bit is not meaningful on Windows, and MinGW stat() derives it from the file extension). Wrap the stat/S_IXUSR assertion in #ifndef _WIN32 so test-windows passes; the content and creation assertions still run on all platforms. Refs DeusData#472 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Summary
install(especially with--force) only ad-hoc-signed whatever binary already sat at~/.local/bin/codebase-memory-mcpand never copied the binary the operator ran install from. An operator who built from source and raninstall -y --forcewas silently left with the OLD binary at the target — running stale code while believing they had upgraded.This makes
installmirror whatupdatealready does for the binary swap.What changed
installnow detects the running binary, copies it to the canonical target~/.local/bin/codebase-memory-mcp, makes it executable, and ad-hoc signs the target — not whatever the operator happened to launch.cbm_copy_fileopens the destination"wb"before reading the source, so copying a file onto itself would truncate it to zero. A newcbm_same_file()check skips the copy when the running binary already is the target (the normal release-install case), preserving prior behavior.--forceor an interactive confirmation.--dry-runreports the intended swap and touches nothing.update).Secondary issue (linker-signed manual copies)
The copy+sign path also resolves the secondary report: a freshly clang-built arm64 binary is
adhoc, linker-signed(flags=0x20002) and isKilled: 9when spawned by another process (e.g. an MCP host), even though it runs fine from a shell. Re-signing ad-hoc (flags=0x2) makes it launchable —installnow does this for the binary it places, so the manualcp+codesign --force --sign -workaround is no longer required.Implementation
src/cli/cli.c— newcbm_same_file()andcbm_copy_binary_to_target()helpers;cbm_cmd_install()now does copy → sign(target) → configs(target).src/cli/cli.h— declarecbm_copy_binary_to_target().tests/test_cli.c— two regression tests:cli_install_copies_binary_to_target_issue472(fresh + stale-overwrite copy, exec bit) andcli_install_same_file_guard_issue472(self-copy must not truncate, incl. a symlink/same-inode case).Test coverage note
The regression tests target the extracted
cbm_copy_binary_to_target()seam rather thancbm_cmd_install()directly. A fullcbm_cmd_install()invocation is deliberately avoided in unit tests because it has process-global side effects (it SIGTERMs othercodebase-memory-mcpinstances) and HOME/PATH/config coupling that don't isolate cleanly in the suite. As a result the--force/prompt branch, the--dry-run"would" messaging, and the config-target redirection are validated by reading/review rather than automated tests; the copy + same-file-guard logic (the actual bug) is covered by the helper-level tests.Validation
scripts/build.sh— clean (-Werror).clang-format— clean on all changed files.scripts/test.sh— 5605 passed; the single remaining failure (cli_hook_gate_script_no_predictable_tmp_issue384) is pre-existing onmain(confirmed identical baseline without this change) and unrelated to this PR. Both newissue472tests pass.Closes #472
🤖 Generated with Claude Code