fix(clients): resolve mempalace-mcp.py via readlink, not absolute path#10
fix(clients): resolve mempalace-mcp.py via readlink, not absolute path#10jphein wants to merge 1 commit intorboarescu:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes the MCP dispatcher script so daemon-proxy mode runs the sibling mempalace-mcp.py relative to the dispatcher’s location instead of a machine-specific absolute path.
Changes:
- Replaces hardcoded
exectarget with a path computed from the dispatcher’s resolved location (readlink -f+dirname). - Adds brief inline documentation explaining the sibling-path resolution.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # CLAUDE_PLUGIN_ROOT or directly via an absolute path or symlink). | ||
|
|
||
| PYTHON="${MEMPALACE_PYTHON:-python3}" | ||
| HERE="$(cd -- "$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")" &> /dev/null && pwd)" |
There was a problem hiding this comment.
readlink -f is GNU-specific and fails on macOS (BSD readlink has no -f). This breaks the stated “works … on symlink/absolute path” behavior for macOS clients. Consider using a portable resolver (e.g., python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "${BASH_SOURCE[0]}"), or a realpath/readlink fallback chain when readlink -f is unavailable.
| HERE="$(cd -- "$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")" &> /dev/null && pwd)" | |
| SCRIPT_PATH="$("$PYTHON" -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "${BASH_SOURCE[0]}")" | |
| HERE="$(cd -- "$(dirname -- "$SCRIPT_PATH")" &> /dev/null && pwd)" |
There was a problem hiding this comment.
Done — switched from readlink -f (GNU-specific, fails on macOS BSD) to cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd -P (POSIX-portable, resolves symlinks in the directory path). Header comment at lines 6-14 documents the choice. Verified manually on macOS (BSD readlink lacks -f).
| HERE="$(cd -- "$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")" &> /dev/null && pwd)" | ||
| MCP_CLIENT="$HERE/mempalace-mcp.py" | ||
|
|
||
| if [ -n "$PALACE_DAEMON_URL" ]; then |
There was a problem hiding this comment.
If HERE fails to resolve (e.g., readlink -f unavailable) or the script is packaged without the sibling mempalace-mcp.py, exec "$PYTHON" "$MCP_CLIENT" ... will fail with a generic Python “can’t open file” error. Adding an explicit check that $MCP_CLIENT exists (and a clear error message) would make failures easier to diagnose.
| if [ -n "$PALACE_DAEMON_URL" ]; then | |
| if [ -n "$PALACE_DAEMON_URL" ]; then | |
| if [ ! -f "$MCP_CLIENT" ]; then | |
| echo "Error: mempalace MCP client script not found: $MCP_CLIENT" >&2 | |
| exit 1 | |
| fi |
There was a problem hiding this comment.
Addressed — explicit error path before the exec: if [ ! -f "$MCP_CLIENT" ]; then echo "palace-mcp-dispatch: missing sibling client at $MCP_CLIENT" >&2; echo " expected mempalace-mcp.py to live next to this script." >&2; exit 1; fi. Operators now see a clear daemon-dispatch-specific message instead of a generic Python "can't open file" trace.
`clients/palace-mode` shipped with two embedded user-specific defaults
on lines 19-20: a `DEFAULT_URL` pointing at my homelab daemon and a
real hex `DEFAULT_KEY`. Both are leftovers from when the script was
extracted from my own install — the URL is my homelab address, and
the hex string is a real API key I authored (it has since been
rotated, so it doesn't authenticate against my daemon anymore — but
the literal value is still in upstream main's source).
## Fix
Read both from environment, no in-source defaults:
```bash
DEFAULT_URL="${PALACE_DAEMON_URL_DEFAULT:-${PALACE_DAEMON_URL:-}}"
DEFAULT_KEY="${PALACE_API_KEY:-}"
```
Plus fail-fast guards in `set_mode_remote()` so silent empty-string
writes don't end up in `~/.claude/settings.local.json`. If either URL
or key is unset, the script prints a clear error pointing at the right
env vars and exits 2 instead of writing broken config.
## Why now
Three reasons even though the embedded key is no longer live:
1. Trains contributors that hex defaults are normal — anyone reading
the source might assume embedding their own key is fine.
2. Default `palace-mode remote` (no args) writes my homelab URL into
a fresh user's `settings.local.json`, sending their hooks to a
daemon they have no auth for.
3. Reviewable provenance — removing the literal makes future audits
cleaner.
## Scope
+11 / -1. Single file. No new dependencies. No behavior change for
operators who set `PALACE_DAEMON_URL` and `PALACE_API_KEY` in their
shell rc. Operators who relied on the embedded defaults get a clear
error pointing at what they need to set.
Originally part of fork commit `d450cef` ("address upstream review on
PR #4"); the portable-paths half of that commit shipped as PR rboarescu#10.
Sending the embedded-defaults half standalone.
The dispatcher in `clients/palace-mcp-dispatch.sh` has a hardcoded `/home/jp/Projects/palace-daemon/clients/mempalace-mcp.py` in the `--daemon` branch — a leftover from when it was extracted into PR #4. As shipped in `main` today, the dispatcher only works on my machine; on every other operator's machine the `exec` target doesn't exist and the daemon-proxy mode fails immediately. ## Fix Resolve the sibling client path at runtime: ```bash HERE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd -P)" MCP_CLIENT="$HERE/mempalace-mcp.py" ``` `cd "$(dirname …)" && pwd -P` is POSIX-portable. It resolves symlinks in the directory path via the filesystem, which covers the actual common case for plugin invocations (the script's parent directory may be a symlink under a Claude Code plugin cache). The earlier `readlink -f` approach failed on macOS — BSD `readlink` doesn't accept `-f`, so the dispatcher would silently `command not found` on macOS clients (caught in Copilot review). Also adds an explicit existence check on `$MCP_CLIENT` before exec, so the failure mode is a clear error message ("missing sibling client at …") rather than a generic Python "can't open file" trace. Same behavior on my machine; correct + portable behavior everywhere else. The "in-process" branch (`python -m mempalace.mcp_server`) was already portable, no change there. Originally extracted from a fork-side commit (`d450cef`) that landed during PR #4's review cycle but didn't make it into the final cherry-pick. Sending standalone per the "small separate PRs" preference.
2b78e25 to
7d02e2b
Compare
`clients/palace-mode` shipped with two embedded user-specific defaults
on lines 19-20: a `DEFAULT_URL` pointing at my homelab daemon and a
real hex `DEFAULT_KEY`. Both are leftovers from when the script was
extracted from my own install — the URL is my homelab address, and
the hex string is a real API key I authored (it has since been
rotated, so it doesn't authenticate against my daemon anymore — but
the literal value is still in upstream main's source).
## Fix
Read both from environment, no in-source defaults:
```bash
DEFAULT_URL="${PALACE_DAEMON_URL_DEFAULT:-${PALACE_DAEMON_URL:-}}"
DEFAULT_KEY="${PALACE_API_KEY:-}"
```
Plus fail-fast guards in `set_mode_remote()` so silent empty-string
writes don't end up in `~/.claude/settings.local.json`. If either URL
or key is unset, the script prints a clear error pointing at the right
env vars and exits 2 instead of writing broken config.
## Why now
Three reasons even though the embedded key is no longer live:
1. Trains contributors that hex defaults are normal — anyone reading
the source might assume embedding their own key is fine.
2. Default `palace-mode remote` (no args) writes my homelab URL into
a fresh user's `settings.local.json`, sending their hooks to a
daemon they have no auth for.
3. Reviewable provenance — removing the literal makes future audits
cleaner.
## Scope
+11 / -1. Single file. No new dependencies. No behavior change for
operators who set `PALACE_DAEMON_URL` and `PALACE_API_KEY` in their
shell rc. Operators who relied on the embedded defaults get a clear
error pointing at what they need to set.
Originally part of fork commit `d450cef` ("address upstream review on
PR #4"); the portable-paths half of that commit shipped as PR rboarescu#10.
Sending the embedded-defaults half standalone.
…boarescu#10/rboarescu#12/rboarescu#13 to main The amendments to those open upstream PRs caught real bugs that exist on fork main too — production at disks runs fork main, so these were shipping live until now. ## main.py — /graph endpoint (from PR rboarescu#13) - `_read_wings_rooms_direct()`: build `rooms` from union of `wings.keys()` + `rooms_by_wing.keys()` rather than just wings. Otherwise a partial schema-drift (one query OperationalError-ed but the other succeeded) silently drops the half that worked. Real data-loss bug for the SME / /viz consumers. - `/graph` handler: gate the direct-sqlite reads on `_read_sem` via a `_direct_under_sem` helper. Coordinates with `_exclusive_palace()` used by `/repair mode=rebuild` (so /graph yields rather than racing delete-then-create on chroma.sqlite3) and rate-limits unbounded `asyncio.to_thread` reads under load (was 2 threads/request × N concurrent requests). - `_canonical_topic()` (from PR rboarescu#8): drop the `topic: str` annotation and add a non-string isinstance guard at the top, coerce `None`/numbers/lists to `CHECKPOINT_TOPIC` with a warning. A malformed `{"topic": null}` JSON payload no longer leaks `None` through to `tool_diary_write`. ## scripts/verify-routes.sh (from PR rboarescu#9) - `set -euo pipefail` (was `set -e`) — pipeline failures and unset vars now surface instead of being masked. - `curl -fsS` instead of `-sS` on every probe so HTTP non-2xx (e.g. `/health` 503 when palace is degraded) fails curl directly. Without this, a 503 body with `"daemon":"palace-daemon"` slips through the body-grep as a pass. - Limit-check rewrite: split curl exit code from JSON parse so a connection error or non-JSON response `?`-flags rather than fails-with-traceback (the previous `2>&1` redirect captured Python tracebacks into `$COUNT`, which then triggered the fail path with a confusing message). ## clients/palace-mcp-dispatch.sh (from PR rboarescu#10) - Replace `readlink -f` (GNU-specific, fails on macOS BSD readlink) with `cd "$(dirname …)" && pwd -P`. POSIX-portable; covers the actual common case (parent directory is a symlink under the Claude Code plugin cache). - Add explicit existence check on `$MCP_CLIENT` before exec, with a clear error message instead of a generic Python "can't open file" trace. ## clients/palace-mode (from PR rboarescu#12) - Make `PALACE_API_KEY` optional in `set_mode_remote`. Server-side `_check_auth` only enforces auth when its own `PALACE_API_KEY` is set, so a daemon running without auth accepts unauthenticated clients. The previous fail-fast was overly strict for that case. Warn-but-proceed instead. All five fixes are amendments on the corresponding upstream PR branches (force-pushed earlier today). No behavior change for the healthy path; the changes harden failure modes that were caught in review.
Bug
The dispatcher in
clients/palace-mcp-dispatch.shhas a hardcoded/home/jp/Projects/palace-daemon/clients/mempalace-mcp.pyin the--daemonbranch:That's my install path — accidentally embedded when this script was extracted into PR #4. As shipped in
maintoday, the dispatcher only works on my machine; on every other operator's machine theexectarget doesn't exist and the daemon-proxy mode fails immediately.Fix
Replace with a portable sibling lookup:
readlink -fthe script, take the dirname, build the sibling path.Works regardless of how the script was invoked — symlinked, absolute path, relative path, plugin-cached. The "in-process" branch (
python -m mempalace.mcp_server) was already portable, no change needed there.Scope
+6 / -1. No new dependencies. Same behavior on my machine; correct behavior everywhere else.Test plan
./clients/palace-mcp-dispatch.sh— unset PALACE_DAEMON_URL, expects in-process path (unchanged).mempalace-mcp.py. Verify this resolves to the right file regardless of where the dispatcher was invoked from (symlink, absolute path, etc.).cd -- ... &> /dev/null && pwdidiom handles paths with spaces correctly; tested by placing the repo under a path with a space.Originally extracted from a fork-side commit (
d450cef) that landed during PR #4's review cycle but didn't make it into the final cherry-pick. Sending standalone per the "small separate PRs" preference.