diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bd2560..17d87f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,73 @@ # Changelog +## [Unreleased] + +### Fixed +- **Installer trampolines blocked by Windows Defender ASR** — + ``install.ps1`` and ``install.sh`` invoked the + ``truememory-mcp --setup`` and ``truememory-ingest install`` console- + script shims directly. Those shims are setuptools / uv trampolines with + a per-install unique SHA-256, which Microsoft Defender's Attack-Surface- + Reduction rule + ``01443614-cd74-433a-b99e-2ecdc07bfc25`` ("Block executable files from + running unless they meet a prevalence, age, or trusted list criteria") + silently kills at ``CreateProcess`` time on hardened-dev-box + configurations: the binary has zero machines worth of MS cloud + prevalence so the launch is blocked before any user code runs. (The + rule defaults to Audit-only per DISA STIG and CIS Benchmark, but a + growing share of Windows-11 hardened-baseline images run it in Block + mode.) Switched both installers to invoke ``$toolPython -m + truememory.mcp_server --setup`` / ``$toolPython -m + truememory.ingest.cli install`` — the routing through the PSF-signed + high-prevalence ``python.exe`` is invisible to ASR. Same mechanic the + ``mcp_server._setup_claude`` writer already uses when it registers the + MCP server with Claude Code / Claude Desktop, so the installer is now + consistent with the runtime config it produces. Added a Windows-ASR + troubleshooting section to the installer's "done" banner and to the + README so users on the rare *Block*-mode hosts can re-run the + equivalent module form manually if upgrading. +- **``_setup_claude`` auto-migrates stale shim paths in Claude config** — + users who had a prior install where the Claude Code / Claude Desktop + MCP server entry was registered with a bare ``truememory-mcp`` shim + path now get a one-time migration on the next ``--setup``: the + existing entry is detected as a setuptools console-script shim, the + registration is removed and re-added with the + ``[python_path, "-m", "truememory.mcp_server"]`` form. The previous + "existing config preserved" branch kept those stale shim paths in + place, which on ASR Block-mode Windows hosts meant every Claude + Desktop launch tried to spawn the blocked binary. Migration is + detected by suffix (``/truememory-mcp.exe`` or ``/truememory-mcp``) + and by canonical install-dir substrings (``/scripts/truememory-mcp``, + ``/bin/truememory-mcp``) so it works regardless of OS or install + method. Reported as "migrated from shim to python -m form" in the + setup output so the user can see what changed. +- **Installer no longer aborts when tool venv python is unresolvable** — + the early ``Die`` introduced alongside the ASR fix made + ``TRUEMEMORY_SKIP_SETUP=1`` unusable: if the uv-tool venv layout + didn't match the expected ``Scripts/python.exe`` / + ``bin/python`` path, the installer aborted before honouring the skip. + Softened to a ``Warn`` that skips steps 4 and 5 with an actionable + re-run hint, matching the original installer's behaviour for the + model-download step. +- **install.sh missing ``set -o pipefail``** — partial mid-pipeline + failures previously returned 0 and let the installer print "Installed + successfully" on a broken install. Added. +- **install.ps1 ``$PKG_SPEC`` unquoted** — paths with spaces in + ``TRUEMEMORY_SOURCE`` were split into multiple arguments by + PowerShell's parser. Quoted. +- **install.ps1 ``uv tool uninstall`` exit code unchecked** — a real + uninstall failure (locked files, permission issue) was silently + swallowed and the subsequent install masked it. Now warns on exit + > 1 (exit 1 just means "not installed" and is expected on fresh + boxes). +- **README Step 4 Windows tray-quit instruction** — Windows users + closing all Claude windows but leaving Claude Desktop running in the + system tray would never see the MCP config reload, since the config + only loads at a full process launch. Updated to direct users to + right-click the tray icon and Quit. +- **README BibTeX citation version stale** — bumped from ``0.6.0`` to + ``0.6.8`` to match ``pyproject.toml``. + ## [0.6.8] — 2026-05-11 ### Fixed diff --git a/README.md b/README.md index a83f32d..48ac22f 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ ${\color{#1a73e8}\textbf{\textsf{Step 3.}}}$ Wait 3-5 minutes for installation.   -${\color{#1a73e8}\textbf{\textsf{Step 4.}}}$ Quit Claude completely and reopen it (Mac: `Cmd+Q`, Windows: close all Claude windows). +${\color{#1a73e8}\textbf{\textsf{Step 4.}}}$ Quit Claude completely and reopen it (Mac: `Cmd+Q`, Windows: right-click the Claude icon in the system tray → **Quit** — clicking X only minimizes it, the MCP config only loads at a full launch).   @@ -82,6 +82,19 @@ That's it. TrueMemory remembers your conversations automatically from here. Installs [uv](https://docs.astral.sh/uv/) (Astral's Python tool manager) if needed, fetches a managed Python 3.12, installs TrueMemory with all tier models into an isolated tool environment, registers the MCP server, wires up lifecycle hooks, and merges instructions into `~/.claude/CLAUDE.md`. Your system Python is never touched. No sudo, no venvs, no pip struggle. +#### Windows: hardened ASR baselines + +If you're on a Windows host that has Microsoft Defender's Attack-Surface-Reduction rule [01443614-cd74-433a-b99e-2ecdc07bfc25](https://learn.microsoft.com/en-us/defender-endpoint/attack-surface-reduction-rules-reference#block-executable-files-from-running-unless-they-meet-a-prevalence-age-or-trusted-list-criterion) set to **Block** (rather than the default Audit), the `truememory-mcp.exe` and `truememory-ingest.exe` shims may be silently killed at launch — they're setuptools/uv trampolines with a per-install unique hash, so they fail the cloud-prevalence check. + +The installer routes around this by invoking the module form (`python -m truememory.mcp_server`) through the signed `python.exe` wrapper. If you ever need to re-run setup manually on a Block-mode host: + +```powershell +python -m truememory.mcp_server --setup +python -m truememory.ingest.cli install +``` + +This is the same form the installer writes into Claude Code's MCP config, so the running server is unaffected — only the bare-shim invocations are. + #### Audit the script It's ~200 lines of shell, no sudo, stays entirely under `$HOME`: @@ -359,7 +372,7 @@ Find me on X [@Building_Josh](https://x.com/Building_Josh) · Follow us [@Sauron organization = {Sauron}, year = {2026}, url = {https://github.com/buildingjoshbetter/TrueMemory}, - version = {0.6.0} + version = {0.6.8} } ``` diff --git a/install.ps1 b/install.ps1 index 0c30e04..1022aec 100644 --- a/install.ps1 +++ b/install.ps1 @@ -87,8 +87,14 @@ if ($LASTEXITCODE -ne 0) { # ---------- step 3: install truememory as a uv tool ---------- Say "installing $PKG_SPEC (~3-5 min on first run, downloads all tier models)..." -& uv tool uninstall truememory *> $null -& uv tool install --python $TRUEMEMORY_PY --force --refresh $PKG_SPEC > $null +# uninstall: exit 1 just means "not currently installed" — that's the +# common case on a fresh box. Anything higher is a real problem (locked +# file, permissions) and worth surfacing. +& uv tool uninstall truememory 2>$null *> $null +if ($LASTEXITCODE -gt 1) { + Warn "uv tool uninstall returned $LASTEXITCODE — proceeding with install, but the result may be partial (try closing any running truememory-mcp processes)" +} +& uv tool install --python $TRUEMEMORY_PY --force --refresh "$PKG_SPEC" > $null if ($LASTEXITCODE -ne 0) { Die "truememory install failed" } @@ -107,20 +113,52 @@ if ($uvToolDir) { } } +# Resolve the tool venv's python.exe up front. We invoke `python.exe -m +# ` for every subsequent step instead of the bare `truememory-mcp` +# / `truememory-ingest` console-script shims. +# +# Why: those `.exe` shims are setuptools/uv trampolines with a unique +# per-install hash. Windows Defender's ASR rule +# 01443614-cd74-433a-b99e-2ecdc07bfc25 ("Block executable files from +# running unless they meet a prevalence, age, or trusted list criteria") +# silently blocks them at launch on hardened-dev-box configurations +# because the cloud-prevalence check fails. Routing through `python.exe` +# (signed by the PSF / Astral Python distribution) bypasses the check — +# python.exe is high-prevalence and trusted. +# +# Missing-toolPython is a `Warn`, not a `Die`: the user may have set +# TRUEMEMORY_SKIP_SETUP=1 expecting to configure Claude themselves, and +# even when not, we'd rather finish the install + tell them the exact +# manual command than abort halfway through. +# +# See: https://learn.microsoft.com/en-us/defender-endpoint/attack-surface-reduction-rules-reference +$toolPython = $null +if ($uvToolDir) { + $candidate = Join-Path $uvToolDir "truememory\Scripts\python.exe" + if (Test-Path $candidate) { + $toolPython = $candidate + } +} + # ---------- step 4: auto-configure Claude ---------- if ($env:TRUEMEMORY_SKIP_SETUP -eq "1") { Say "skipping Claude setup (TRUEMEMORY_SKIP_SETUP=1)" +} elseif (-not $toolPython) { + Warn "could not locate the truememory tool venv python at $uvToolDir\truememory\Scripts\python.exe — skipping Claude setup." + Warn "Re-run manually after restarting your terminal:" + Warn " python -m truememory.mcp_server --setup" + Warn " python -m truememory.ingest.cli install" } else { Say "configuring Claude Code / Claude Desktop..." - & truememory-mcp --setup + & $toolPython -m truememory.mcp_server --setup if ($LASTEXITCODE -ne 0) { - Warn "auto-setup returned non-zero (you can re-run it with: truememory-mcp --setup)" + Warn "auto-setup returned non-zero (you can re-run it with: python -m truememory.mcp_server --setup)" } Say "installing hooks and CLAUDE.md instructions..." - & truememory-ingest install + & $toolPython -m truememory.ingest.cli install if ($LASTEXITCODE -ne 0) { - Warn "hook install returned non-zero (you can re-run it with: truememory-ingest install)" + Warn "hook install returned non-zero (you can re-run it with: python -m truememory.ingest.cli install)" } } @@ -128,8 +166,7 @@ if ($env:TRUEMEMORY_SKIP_SETUP -eq "1") { Say "pre-downloading models for all tiers (Edge + Base + Pro)..." Say " this takes 2-5 min but means tier switching just works afterward." -$toolPython = Join-Path (& uv tool dir 2>$null) "truememory\Scripts\python.exe" -if (Test-Path $toolPython) { +if ($toolPython) { Say " [1/3] Edge reranker (MiniLM-L-6-v2, ~22MB)..." & $toolPython -c "from sentence_transformers import CrossEncoder; CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')" if ($LASTEXITCODE -eq 0) { Ok " [1/3] Edge reranker ready" } @@ -188,3 +225,10 @@ Write-Host "" Write-Host " Note:" -ForegroundColor Yellow -NoNewline Write-Host " If commands are not found, close and reopen PowerShell." Write-Host "" +Write-Host " If Windows Defender blocks ``truememory-mcp.exe`` / ``truememory-ingest.exe``" -ForegroundColor Yellow +Write-Host " with 'Block executable files from running unless they meet a prevalence," -ForegroundColor Yellow +Write-Host " age, or trusted list criteria' (ASR rule 01443614), use the module form" -ForegroundColor Yellow +Write-Host " instead — the python.exe wrapper is signed and passes ASR:" -ForegroundColor Yellow +Write-Host " python -m truememory.mcp_server --setup " -NoNewline; Write-Host "# re-run Claude auto-config" -ForegroundColor DarkGray +Write-Host " python -m truememory.ingest.cli install " -NoNewline; Write-Host "# re-install hooks" -ForegroundColor DarkGray +Write-Host "" diff --git a/install.sh b/install.sh index 8a995f2..f964005 100755 --- a/install.sh +++ b/install.sh @@ -44,7 +44,11 @@ die() { warn "error: $*"; exit 1; } # ---------- main ---------- main() { - set -eu + # pipefail catches mid-pipeline failures that `set -e` alone misses + # (e.g. a network drop inside `curl ... | sh` where curl returns 0 + # but sh aborts partway). Bash-only but the script already uses + # `$(...)` and `||` constructs that assume a modern shell. + set -euo pipefail TRUEMEMORY_PY="${TRUEMEMORY_PY:-3.12}" TRUEMEMORY_EXTRAS="${TRUEMEMORY_EXTRAS:-}" @@ -113,19 +117,45 @@ main() { say "adding uv's tool dir to your shell rc (reversible)..." uv tool update-shell >/dev/null 2>&1 || true + # Resolve the tool venv's python up front so steps 4 and 5 can invoke + # `python -m truememory.mcp_server` / `python -m truememory.ingest.cli` + # directly instead of the `truememory-mcp` / `truememory-ingest` console- + # script shims. + # + # Why: on Windows, those `.exe` shims are setuptools/uv trampolines with + # a unique per-install hash, which Microsoft Defender's ASR rule + # 01443614 ("Block executable files from running unless they meet a + # prevalence, age, or trusted list criteria") blocks on hardened-dev-box + # configurations. We keep the install.sh path consistent with install.ps1 + # so a single canonical invocation works across all platforms — even + # though POSIX doesn't have ASR, the shim is the only difference and + # it's a brittle dependency on $PATH resolution timing. + # + # Missing TOOL_PYTHON is a `warn`, not a `die`: the user may have set + # TRUEMEMORY_SKIP_SETUP=1 and skipped the Claude-config step entirely, + # in which case dying here would block the model-download step they + # likely still want. + TOOL_PYTHON="$(uv tool dir)/truememory/bin/python" + # ---------- step 4: auto-configure Claude ---------- if [ "${TRUEMEMORY_SKIP_SETUP:-}" = "1" ]; then say "skipping Claude setup (TRUEMEMORY_SKIP_SETUP=1)" + elif [ ! -x "$TOOL_PYTHON" ]; then + warn "could not locate tool venv python at $TOOL_PYTHON — skipping Claude setup." + warn "Re-run manually after opening a new terminal:" + warn " python -m truememory.mcp_server --setup" + warn " python -m truememory.ingest.cli install" else say "configuring Claude Code / Claude Desktop..." - # truememory-mcp lives at ~/.local/bin/truememory-mcp. Its sys.executable - # resolves to the isolated tool venv, so Claude gets a stable absolute path. - truememory-mcp --setup || \ - warn "auto-setup returned non-zero (you can re-run it with: truememory-mcp --setup)" + # Invoke via `python -m truememory.mcp_server` — see comment above for + # the Windows ASR rationale. The module's `if __name__ == '__main__'` + # block routes `--setup` through the same code path as the shim. + "$TOOL_PYTHON" -m truememory.mcp_server --setup || \ + warn "auto-setup returned non-zero (you can re-run it with: python -m truememory.mcp_server --setup)" say "installing hooks and CLAUDE.md instructions..." - truememory-ingest install || \ - warn "hook install returned non-zero (you can re-run it with: truememory-ingest install)" + "$TOOL_PYTHON" -m truememory.ingest.cli install || \ + warn "hook install returned non-zero (you can re-run it with: python -m truememory.ingest.cli install)" fi # ---------- step 5: pre-download models for all tiers ---------- @@ -135,7 +165,6 @@ main() { # Use the tool's Python to run the download inside the uv venv. # stderr is NOT suppressed — HuggingFace's tqdm progress bars show # download percentage, speed, and ETA, which is better UX than silence. - TOOL_PYTHON="$(uv tool dir)/truememory/bin/python" if [ -x "$TOOL_PYTHON" ]; then # Edge: Model2Vec embedder (usually bundled) + MiniLM reranker say " [1/3] Edge reranker (MiniLM-L-6-v2, ~22MB)..." diff --git a/truememory/mcp_server.py b/truememory/mcp_server.py index dcbe604..cf65de0 100644 --- a/truememory/mcp_server.py +++ b/truememory/mcp_server.py @@ -1288,6 +1288,30 @@ def _path_exists(p: str) -> bool: except Exception: return False + def _is_shim_path(cmd: str) -> bool: + """Return True if ``cmd`` is a setuptools / uv console-script shim + for ``truememory-mcp``. + + Why we care: those shims have a per-install unique SHA-256 hash + and Windows Defender's ASR rule 01443614 ("Block executable files + from running unless they meet a prevalence, age, or trusted list + criteria") silently kills them at launch on hardened Win11 + baselines. The canonical workaround is to invoke the same code + through the high-prevalence signed ``python.exe`` instead. When + ``--setup`` runs on a machine that already had a shim path baked + into the Claude config from a previous install, we migrate it + rather than preserve it. + """ + if not cmd: + return False + lower = cmd.lower().replace("\\", "/") + return ( + lower.endswith("/truememory-mcp.exe") + or lower.endswith("/truememory-mcp") + or "/scripts/truememory-mcp" in lower + or "/bin/truememory-mcp" in lower + ) + # --- Claude Code CLI --- claude_bin = shutil.which("claude") if claude_bin: @@ -1323,17 +1347,30 @@ def _path_exists(p: str) -> bool: existing_cmd = tokens[0] break - if _path_exists(existing_cmd): - # Working entry — preserve it (don't clobber a dev venv). + if _is_shim_path(existing_cmd): + # ASR-vulnerable shim path baked in by a previous install + # — migrate to `python -m truememory.mcp_server`. + _run_claude([claude_bin, "mcp", "remove", "--scope", "user", "truememory"]) + retry = _run_claude(add_cmd) + if retry is not None and retry.returncode == 0: + configured.append("Claude Code (migrated from shim to python -m form)") + elif retry is not None: + print(f" Claude Code: migration failed — {retry.stderr.strip()}", file=sys.stderr) + elif _path_exists(existing_cmd): + # Working entry pointing at a real file — preserve it + # (don't clobber a dev venv). configured.append("Claude Code (existing config preserved)") else: - # Stale entry — remove and re-add. + # Empty / unparseable / stale entry — remove and re-add. + # Treating empty as stale (rather than preserve) is the + # safer default: a parse miss + preserve would leave a + # broken entry in place with no diagnostic. _run_claude([claude_bin, "mcp", "remove", "--scope", "user", "truememory"]) retry = _run_claude(add_cmd) if retry is not None and retry.returncode == 0: configured.append("Claude Code (stale entry replaced)") elif retry is not None: - print(f" Claude Code: update failed — {retry.stderr.strip()}") + print(f" Claude Code: update failed — {retry.stderr.strip()}", file=sys.stderr) else: print(f" Claude Code: failed — {result.stderr.strip()}") @@ -1359,8 +1396,13 @@ def _path_exists(p: str) -> bool: servers["truememory"] = {"command": python_path, "args": list(mcp_args)} desktop_config_path.write_text(json.dumps(config, indent=2), encoding="utf-8") configured.append("Claude Desktop") + elif _is_shim_path(existing_cmd): + # ASR-vulnerable shim path — migrate to `python -m`. + servers["truememory"] = {"command": python_path, "args": list(mcp_args)} + desktop_config_path.write_text(json.dumps(config, indent=2), encoding="utf-8") + configured.append("Claude Desktop (migrated from shim to python -m form)") elif _path_exists(existing_cmd): - # Working entry — preserve it. + # Working entry pointing at a real file — preserve it. configured.append("Claude Desktop (existing config preserved)") else: # Stale entry — replace it. @@ -1368,7 +1410,7 @@ def _path_exists(p: str) -> bool: desktop_config_path.write_text(json.dumps(config, indent=2), encoding="utf-8") configured.append("Claude Desktop (stale entry replaced)") except Exception as e: - print(f" Claude Desktop: failed — {e}") + print(f" Claude Desktop: failed — {e}", file=sys.stderr) # --- Report --- print()