From c745d118708d3ff73ff59782ba717c745135c29f Mon Sep 17 00:00:00 2001 From: Eric Garcia Date: Thu, 5 Feb 2026 21:01:19 -0500 Subject: [PATCH] feat: RFC 0060 reliable binary installation - install.sh: Add xattr/codesign fix after cp on macOS - install.sh: Add portable timeout verification using perl - INSTALL.md: Recommend cargo install --path as primary method - INSTALL.md: Document macOS signature issue and fix - blue doctor: Detect com.apple.provenance xattr with fix hint - blue doctor: Verify code signature with codesign --verify - blue doctor: 3-second liveness timeout for hanging binaries Fixes dyld hang at _dyld_start when copied binaries have stale signatures after Homebrew library updates. Co-Authored-By: Claude Opus 4.5 --- ...0-reliable-binary-installation.accepted.md | 135 ++++++++++++ INSTALL.md | 193 +++++++++--------- apps/blue-cli/src/main.rs | 79 ++++++- install.sh | 48 ++++- 4 files changed, 355 insertions(+), 100 deletions(-) create mode 100644 .blue/docs/rfcs/0060-reliable-binary-installation.accepted.md diff --git a/.blue/docs/rfcs/0060-reliable-binary-installation.accepted.md b/.blue/docs/rfcs/0060-reliable-binary-installation.accepted.md new file mode 100644 index 0000000..ee01641 --- /dev/null +++ b/.blue/docs/rfcs/0060-reliable-binary-installation.accepted.md @@ -0,0 +1,135 @@ +# RFC 0060: Reliable Binary Installation + +| | | +|---|---| +| **Status** | Accepted | +| **Date** | 2026-02-06 | +| **Relates To** | RFC 0052 (CLI Hook Management), RFC 0049 (Synchronous Guard) | + +--- + +## Summary + +Fix the binary installation flow so it doesn't produce binaries that hang on macOS. Add `cargo install --path` support as the primary install method, and post-copy re-signing as a fallback safety net. + +## Problem Statement + +### The Bug + +When the Blue binary is copied to `~/.cargo/bin/` (or `/usr/local/bin/`), macOS preserves stale extended attributes (`com.apple.provenance`) and the adhoc code signature from the original build. If Homebrew updates a dynamically linked library (e.g., `openssl@3`) between when the binary was built and when it's run, `dyld` hangs indefinitely at `_dyld_start` during signature verification. The process never reaches `main()`. + +### Observed Behavior + +``` +$ blue init +[hangs forever — no output, 0 bytes written to stdout/stderr] + +$ sample $PID +890 _dyld_start (in dyld) + 0 # 100% of samples stuck here +``` + +- `kill -9` cannot terminate the process (state: `UE` — uninterruptible + exiting) +- The same binary works fine when run from `target/release/blue` directly +- The same binary works fine when copied to `/tmp/` without extended attributes + +### Root Cause + +`cp` on macOS preserves extended attributes by default. The combination of: +1. `com.apple.provenance` xattr (marks binary as "downloaded/copied") +2. Stale adhoc linker signature from original `cargo build` +3. Updated dylib versions on disk (Homebrew `openssl@3`) + +...causes `dyld` to enter a signature verification path that deadlocks. + +### Evidence + +| Test | Result | +|------|--------| +| `target/release/blue init` | Works (0.1s) | +| `cp` to `~/.cargo/bin/blue` then run | Hangs at `_dyld_start` | +| `cp` to `/tmp/blue-copy` then run | Works (no provenance xattr) | +| Symlink to `target/release/blue` | Works | +| `xattr -cr` + `codesign --force --sign -` | Works | + +## Proposal + +### 1. Support `cargo install` as primary method + +Add workspace metadata so `cargo install --path apps/blue-cli` works correctly. This lets Cargo handle the build-and-install atomically, producing a freshly signed binary. + +**INSTALL.md** becomes: +```bash +# Build and install (recommended) +cargo install --path apps/blue-cli + +# Then configure for Claude Code +blue install +``` + +### 2. Post-copy re-signing in `install.sh` + +For users who prefer `install.sh` or `cp`: + +```bash +cp "$BINARY" "$INSTALL_DIR/blue" +# Fix macOS code signature after copy +if [[ "$OSTYPE" == "darwin"* ]]; then + xattr -cr "$INSTALL_DIR/blue" + codesign --force --sign - "$INSTALL_DIR/blue" +fi +``` + +### 3. Post-copy re-signing in `blue install` (Rust) + +The `handle_install_command` currently doesn't copy the binary anywhere — it only sets up hooks, skills, and MCP config. But the `SessionStart` hook adds `target/release/` to PATH, which is fragile. Instead: + +- Add an optional `--binary` flag to `blue install` that copies and re-signs the binary to `~/.cargo/bin/` +- Or detect if running from `target/release/` and warn the user + +### 4. `blue doctor` validation + +Add a check to `blue doctor` that detects: +- Stale code signatures on the installed binary +- Mismatched binary vs source versions +- Presence of `com.apple.provenance` xattr on the binary + +## Implementation Plan + +### Phase 1: Fix `install.sh` (immediate) + +1. Add `xattr -cr` + `codesign --force --sign -` after every `cp` of the binary +2. Add a verification step that actually runs `blue --version` with a timeout + +### Phase 2: Support `cargo install --path` + +1. Verify `cargo install --path apps/blue-cli` works with the current workspace layout +2. Update `INSTALL.md` to recommend `cargo install` as the primary method +3. Update `install.sh` to use `cargo install` instead of `cp` when possible + +### Phase 3: Harden `blue doctor` + +1. Add macOS signature check: `codesign --verify` on the installed binary +2. Add xattr check: warn if `com.apple.provenance` is present +3. Add timeout-based liveness check: run `blue --version` with a 3s timeout + +## Files Changed + +| File | Change | +|------|--------| +| `install.sh` | Add post-copy re-signing for macOS | +| `apps/blue-cli/src/main.rs` | Add doctor checks for signature issues | +| `INSTALL.md` | Recommend `cargo install --path` as primary method | + +## Test Plan + +- [x] `install.sh` produces a working binary on macOS after Homebrew openssl update +- [x] `cargo install --path apps/blue-cli` produces a working binary +- [x] `blue doctor` detects provenance xattr on binary (warns with fix hint) +- [x] `blue doctor` passes on a freshly installed binary +- [x] Install flow works on Linux (no-op for codesign steps via `#[cfg(target_os = "macos")]`) + +--- + +*"Right then. Let's get to it."* + +-- Blue diff --git a/INSTALL.md b/INSTALL.md index 27ba100..2ac40ba 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,144 +1,151 @@ # Installing Blue -## Quick Install +## Quick Install (Recommended) ```bash -./install.sh -``` +# Build and install Blue (handles code signing correctly) +cargo install --path apps/blue-cli -This builds and installs both: -- **Blue CLI** to `/usr/local/bin/blue` -- **Blue MCP** configured for Claude Code +# Configure for Claude Code +blue install +``` Restart Claude Code after installation. +## Alternative: Build Then Install + +If you prefer to build separately: + +```bash +# Build Blue +cargo build --release + +# Install for Claude Code (from build directory) +./target/release/blue install +``` + +Note: Running from `target/release/` works but the binary path may change after `cargo clean`. For a persistent installation, use `cargo install` above. + ## What Gets Installed -### CLI +`blue install` configures everything for Claude Code: -The `blue` command becomes available system-wide: +| Component | Location | Purpose | +|-----------|----------|---------| +| **Hooks** | `.claude/hooks/` | Session lifecycle, write guards | +| **Settings** | `.claude/settings.json` | Project configuration | +| **Skills** | `~/.claude/skills/` | Alignment dialogues | +| **MCP Server** | `~/.claude.json` | Blue tools for Claude | + +### Hooks + +- `session-start.sh` — Injects Blue context at session start +- `guard-write.sh` — Validates file writes (RFC 0038) + +### Skills + +- `alignment-play` — Run multi-expert alignment dialogues +- `alignment-expert` — Marker syntax for expert agents + +### MCP Tools + +After restart, Claude has access to Blue tools: +- `blue_status`, `blue_next` — Project state +- `blue_rfc_*` — RFC management +- `blue_worktree_*` — Git worktree coordination +- `blue_pr_create` — Pull request creation +- `blue_dialogue_*` — Alignment dialogues + +## System-Wide Install + +To make `blue` available everywhere: ```bash -blue --version # Check installation -blue realm status # Realm commands -blue session start # Session management -blue daemon start # Background service +# Recommended: cargo install handles signing correctly +cargo install --path apps/blue-cli + +# The binary is installed to ~/.cargo/bin/blue +# Ensure ~/.cargo/bin is in your PATH ``` -### MCP Server +### Manual Copy (Not Recommended on macOS) -Claude Code configuration is created/updated at `~/.config/claude-code/mcp.json`: - -```json -{ - "mcpServers": { - "blue": { - "command": "blue", - "args": ["mcp"] - } - } -} -``` - -After restart, Claude has access to 8 realm tools: -- `realm_status`, `realm_check`, `contract_get` -- `session_start`, `session_stop` -- `realm_worktree_create`, `realm_pr_status` -- `notifications_list` - -## Manual Install - -### Build +If you must copy the binary manually on macOS, you need to fix the code signature: ```bash +# Build cargo build --release -``` -### Install CLI - -```bash -# Standard location +# Copy and fix signature sudo cp target/release/blue /usr/local/bin/ - -# Or custom location -cp target/release/blue ~/bin/ +sudo xattr -cr /usr/local/bin/blue +sudo codesign --force --sign - /usr/local/bin/blue ``` -### Configure MCP - -Create `~/.config/claude-code/mcp.json`: - -```json -{ - "mcpServers": { - "blue": { - "command": "blue", - "args": ["mcp"] - } - } -} -``` - -If blue isn't in PATH, use the full path: - -```json -{ - "mcpServers": { - "blue": { - "command": "/path/to/blue", - "args": ["mcp"] - } - } -} -``` - -## Custom Install Location - -```bash -INSTALL_DIR=~/bin ./install.sh -``` +Without the signature fix, the binary may hang at startup (see RFC 0060). ## Uninstall ```bash -# Remove CLI -sudo rm /usr/local/bin/blue +blue uninstall +``` -# Remove MCP config (or edit to remove blue entry) -rm ~/.config/claude-code/mcp.json +Or manually: + +```bash +# Remove hooks +rm -rf .claude/hooks/ + +# Remove MCP config +# Edit ~/.claude.json and remove "blue" entry + +# Remove skills +rm ~/.claude/skills/alignment-play +rm ~/.claude/skills/alignment-expert + +# Remove binary (if using cargo install) +cargo uninstall blue # Remove Blue data (optional) -rm -rf ~/.blue +rm -rf .blue/ ``` ## Requirements - Rust toolchain (cargo) - macOS, Linux, or Windows with WSL -- Claude Code (for MCP features) +- Claude Code ## Verify Installation ```bash -# CLI -blue --version +# Check installation health +blue doctor +``` -# MCP (in Claude Code) -Human: What realm tools do you have? -Claude: I have realm_status, realm_check, contract_get... +Or in Claude Code: +``` +Human: blue status +Claude: [calls blue_status] Project: blue, Branch: develop... ``` ## Troubleshooting **"command not found: blue"** -- Ensure `/usr/local/bin` is in your PATH -- Or use `INSTALL_DIR=~/bin ./install.sh` and add `~/bin` to PATH +- Use `cargo install --path apps/blue-cli` (adds to ~/.cargo/bin) +- Or run from the project directory: `./target/release/blue install` +- Ensure `~/.cargo/bin` is in your PATH + +**Binary hangs on macOS (no output)** +- This is a code signature issue (RFC 0060) +- Fix: `xattr -cr $(which blue) && codesign --force --sign - $(which blue)` +- Or reinstall with: `cargo install --path apps/blue-cli --force` **MCP tools not appearing in Claude** - Restart Claude Code after installation -- Check `~/.config/claude-code/mcp.json` syntax +- Run `blue doctor` to check configuration - Verify `blue mcp` runs without errors -**Permission denied** -- The installer uses sudo for `/usr/local/bin` -- Or install to a user directory: `INSTALL_DIR=~/bin ./install.sh` +**Hooks not firing** +- Check `.claude/hooks/` exists with executable scripts +- Run `blue install --force` to regenerate hooks diff --git a/apps/blue-cli/src/main.rs b/apps/blue-cli/src/main.rs index b205e40..9b5aad4 100644 --- a/apps/blue-cli/src/main.rs +++ b/apps/blue-cli/src/main.rs @@ -3270,8 +3270,85 @@ async fn handle_doctor_command() -> Result<()> { // Check binary println!("Binary:"); - if let Ok(path) = which::which("blue") { + let binary_path = which::which("blue").ok(); + if let Some(ref path) = binary_path { println!(" ✓ blue found at {}", path.display()); + + // RFC 0060: macOS-specific signature and liveness checks + #[cfg(target_os = "macos")] + { + // Check for stale provenance xattr + let xattr_output = std::process::Command::new("xattr") + .arg("-l") + .arg(path) + .output(); + + if let Ok(output) = xattr_output { + let attrs = String::from_utf8_lossy(&output.stdout); + if attrs.contains("com.apple.provenance") { + println!(" ⚠ com.apple.provenance xattr present (may cause hangs)"); + println!(" hint: xattr -cr {} && codesign --force --sign - {}", + path.display(), path.display()); + issues += 1; + } + } + + // Check code signature validity + let codesign_output = std::process::Command::new("codesign") + .args(["--verify", "--verbose"]) + .arg(path) + .output(); + + if let Ok(output) = codesign_output { + if output.status.success() { + println!(" ✓ Code signature valid"); + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + if stderr.contains("invalid signature") || stderr.contains("modified") { + println!(" ✗ Code signature invalid or stale"); + println!(" hint: codesign --force --sign - {}", path.display()); + issues += 1; + } else if stderr.contains("not signed") { + println!(" - Binary not signed (may be fine)"); + } + } + } + + // Liveness check with timeout + use std::time::Duration; + let liveness = std::process::Command::new(path) + .arg("--version") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn() + .and_then(|mut child| { + // Wait up to 3 seconds + let start = std::time::Instant::now(); + loop { + match child.try_wait() { + Ok(Some(status)) => return Ok(status.success()), + Ok(None) => { + if start.elapsed() > Duration::from_secs(3) { + let _ = child.kill(); + return Ok(false); + } + std::thread::sleep(Duration::from_millis(50)); + } + Err(e) => return Err(e), + } + } + }); + + match liveness { + Ok(true) => println!(" ✓ Binary responds within timeout"), + Ok(false) => { + println!(" ✗ Binary hangs (dyld signature issue)"); + println!(" hint: cargo install --path apps/blue-cli --force"); + issues += 1; + } + Err(e) => println!(" ⚠ Could not run liveness check: {}", e), + } + } } else { println!(" ✗ blue not found in PATH"); issues += 1; diff --git a/install.sh b/install.sh index 04eae1e..d89b37a 100755 --- a/install.sh +++ b/install.sh @@ -30,13 +30,49 @@ else sudo cp "$BINARY" "$INSTALL_DIR/blue" fi -# Verify installation -if command -v blue &> /dev/null; then - echo -e "${GREEN}Installed successfully${NC}" - echo "" - blue --version 2>/dev/null || blue help 2>/dev/null | head -1 || echo "blue installed to $INSTALL_DIR/blue" +# RFC 0060: Fix macOS code signature after copy +# cp preserves stale xattrs and adhoc signatures, causing dyld hangs +if [[ "$OSTYPE" == "darwin"* ]]; then + echo "Fixing macOS code signature..." + if [ -w "$INSTALL_DIR/blue" ]; then + xattr -cr "$INSTALL_DIR/blue" 2>/dev/null || true + codesign --force --sign - "$INSTALL_DIR/blue" 2>/dev/null || true + else + sudo xattr -cr "$INSTALL_DIR/blue" 2>/dev/null || true + sudo codesign --force --sign - "$INSTALL_DIR/blue" 2>/dev/null || true + fi +fi + +# Verify installation with timeout (RFC 0060) +# A hanging binary won't respond in 3 seconds +# Note: Using perl for portable timeout (works on macOS and Linux) +run_with_timeout() { + local timeout_sec=$1 + shift + perl -e 'alarm shift; exec @ARGV' "$timeout_sec" "$@" 2>/dev/null +} + +echo "Verifying installation..." +BLUE_PATH="$INSTALL_DIR/blue" +if [ -x "$BLUE_PATH" ]; then + if run_with_timeout 3 "$BLUE_PATH" --version >/dev/null 2>&1; then + echo -e "${GREEN}Installed successfully${NC}" + "$BLUE_PATH" --version 2>/dev/null || echo "blue installed to $BLUE_PATH" + else + echo -e "${RED}Binary installed but failed verification (possible signing issue)${NC}" + echo "Try: xattr -cr $BLUE_PATH && codesign --force --sign - $BLUE_PATH" + exit 1 + fi +elif command -v blue &> /dev/null; then + if run_with_timeout 3 blue --version >/dev/null 2>&1; then + echo -e "${GREEN}Installed successfully${NC}" + blue --version 2>/dev/null || echo "blue available in PATH" + else + echo -e "${RED}Binary in PATH but failed verification${NC}" + exit 1 + fi else - echo -e "${GREEN}Installed to $INSTALL_DIR/blue${NC}" + echo -e "${GREEN}Installed to $BLUE_PATH${NC}" echo "Add $INSTALL_DIR to PATH if not already present" fi