Skip to content

fix(clients): resolve mempalace-mcp.py via readlink, not absolute path#10

Open
jphein wants to merge 1 commit intorboarescu:mainfrom
jphein:fix/palace-mcp-dispatch-portable-path
Open

fix(clients): resolve mempalace-mcp.py via readlink, not absolute path#10
jphein wants to merge 1 commit intorboarescu:mainfrom
jphein:fix/palace-mcp-dispatch-portable-path

Conversation

@jphein
Copy link
Copy Markdown
Contributor

@jphein jphein commented Apr 27, 2026

Bug

The dispatcher in clients/palace-mcp-dispatch.sh has a hardcoded /home/jp/Projects/palace-daemon/clients/mempalace-mcp.py in the --daemon branch:

exec \"\$PYTHON\" /home/jp/Projects/palace-daemon/clients/mempalace-mcp.py --daemon \"\$PALACE_DAEMON_URL\"

That's my install path — accidentally embedded when this script 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

Replace with a portable sibling lookup: readlink -f the script, take the dirname, build the sibling path.

HERE=\"\$(cd -- \"\$(dirname -- \"\$(readlink -f -- \"\${BASH_SOURCE[0]}\")\")\" &> /dev/null && pwd)\"
MCP_CLIENT=\"\$HERE/mempalace-mcp.py\"

if [ -n \"\$PALACE_DAEMON_URL\" ]; then
  exec \"\$PYTHON\" \"\$MCP_CLIENT\" --daemon \"\$PALACE_DAEMON_URL\"

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

  • Direct: ./clients/palace-mcp-dispatch.sh — unset PALACE_DAEMON_URL, expects in-process path (unchanged).
  • With PALACE_DAEMON_URL set: expects exec to the sibling mempalace-mcp.py. Verify this resolves to the right file regardless of where the dispatcher was invoked from (symlink, absolute path, etc.).
  • The cd -- ... &> /dev/null && pwd idiom 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.

Copilot AI review requested due to automatic review settings April 27, 2026 16:27
jphein added a commit to jphein/palace-daemon that referenced this pull request Apr 27, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 exec target 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.

Comment thread clients/palace-mcp-dispatch.sh Outdated
# 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)"
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

jphein added a commit to jphein/palace-daemon that referenced this pull request Apr 27, 2026
`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.
@jphein jphein force-pushed the fix/palace-mcp-dispatch-portable-path branch from 2b78e25 to 7d02e2b Compare April 27, 2026 17:50
jphein added a commit to jphein/palace-daemon that referenced this pull request Apr 27, 2026
`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.
jphein added a commit to jphein/palace-daemon that referenced this pull request Apr 27, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants