fix(m1): correct two wrong queries in the Cowork probe check

Both were caught by running the check against a state whose answer was already
known from the screen, which is the only reason they were caught at all. A
check that reports PENDING for a thing that has plainly happened is worse than
no check: it invites the reader to distrust the instrument or, worse, to
believe it.

1. It looked for a directory named after the plugin. Cowork installs into
   rpm/plugin_<opaque id>/, so the name appears in no path -- only in
   rpm/manifest.json, which is now what gets read. The old code also used an
   unquoted $(find ...) in a for-loop over a path containing "Application
   Support", so the space split every path in half. That is the exact
   word-splitting failure this machine's shell rules warn about, and it made
   the check report "not installed" while the server was running.

2. It expected ~/Library/Logs/Claude/mcp-server-<name>.log. That naming is for
   Claude Desktop's own connectors; a plugin's MCP server is logged by
   LocalMcpServerManager into main.log. An absent file at a guessed path is a
   statement about the guess, not about the world.

What the corrected check now reads, and what it establishes:

  rpm/manifest.json  -> jobbsok-probe, plugin_01NxHfqM495jtJzjVjRvgWDm,
                        marketplace "My Uploads", installed 2026-09-04T16:48:20Z
  installed copy     -> 4 files, manifest version 0.0.1 (no stale cache)
  main.log 18:48:28  -> Connecting to plugin:jobbsok-probe:probe-tools
                        negotiated protocol version: 2025-11-25
                        Connected ... (1 tools)
  process            -> pid 12997, /usr/local/bin/python3, a child of Claude.app

So a plugin-declared stdio MCP server does spawn and connect on this Intel Mac,
on the host interpreter rather than in the sandbox. The negotiated version also
vindicates one design choice: probe_tools.py echoes the client's protocolVersion
instead of asserting its own default, and the client asked for 2025-11-25, which
is newer than the default the server would otherwise have claimed.

Still open, and deliberately not inferred from the above: whether a tools/call
to probe_ping actually returns. main.log records the connection lifecycle, not
individual calls, so that one is confirmed in the chat or not at all -- the
script says so rather than treating a missing line as an answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-04 18:51:45 +02:00
commit a4e6b44df4

View file

@ -2,14 +2,23 @@
# Report, from the host, how far the Cowork probe has actually got. # Report, from the host, how far the Cowork probe has actually got.
# #
# The probe asks four questions that only the operator can answer in a Cowork # The probe asks four questions that only the operator can answer in a Cowork
# session. But three of them leave traces on this Mac, and a trace is a # session. But the host-MCP one leaves hard traces on this Mac, and a trace is
# measurement where "what did you see in the UI" is a recollection. This script # a measurement where "what did you see in the UI" is a recollection. This
# reads those traces so each step can be verified before the next one starts. # script reads those traces so each step is verified before the next begins.
#
# Two earlier queries in this script were WRONG and the measurement caught
# both; the corrections are written down here so they are not re-derived:
#
# 1. It searched for a directory named after the plugin. Cowork installs into
# rpm/plugin_<opaque id>/, so the name never appears in a path. The
# manifest at rpm/manifest.json is the index; read that instead.
# 2. It expected ~/Library/Logs/Claude/mcp-server-<name>.log. That naming is
# for Claude Desktop's own connectors. A plugin's MCP server is logged by
# LocalMcpServerManager into main.log instead. No file at the guessed path
# meant the guess was wrong, not that nothing had happened.
# #
# It reports; it does not gate. Exit 0 when every check has landed, 1 while any # It reports; it does not gate. Exit 0 when every check has landed, 1 while any
# is still pending, 2 on a broken precondition. # is pending. bash 3.2-clean, ASCII-only, read-only.
#
# bash 3.2-clean, ASCII-only, read-only: it opens nothing and writes nothing.
set -u set -u
@ -18,19 +27,17 @@ REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
PROBE_DOC="$REPO_ROOT/docs/cowork-probe.md" PROBE_DOC="$REPO_ROOT/docs/cowork-probe.md"
FIXTURE="$REPO_ROOT/tests/fixtures/cowork-probe" FIXTURE="$REPO_ROOT/tests/fixtures/cowork-probe"
ARCHIVE="${JOBBSOK_PROBE_ARCHIVE:-/tmp/jobbsok-probe.plugin}" ARCHIVE="${JOBBSOK_PROBE_ARCHIVE:-/tmp/jobbsok-probe.plugin}"
CLAUDE_LOGS="$HOME/Library/Logs/Claude" MAIN_LOG="$HOME/Library/Logs/Claude/main.log"
MCP_LOG="$CLAUDE_LOGS/mcp-server-probe-tools.log" SESSIONS="$HOME/Library/Application Support/Claude/local-agent-mode-sessions"
CLAUDE_SUPPORT="$HOME/Library/Application Support/Claude" PLUGIN_NAME="jobbsok-probe"
SERVER_KEY="plugin:jobbsok-probe:probe-tools"
pending=0 pending=0
say() { printf '%s\n' "$1"; }
done_() { printf ' [DONE] %s\n' "$1"; } done_() { printf ' [DONE] %s\n' "$1"; }
pend() { printf ' [PENDING] %s\n' "$1"; pending=$((pending+1)); } pend() { printf ' [PENDING] %s\n' "$1"; pending=$((pending+1)); }
info() { printf ' %s\n' "$1"; } info() { printf ' %s\n' "$1"; }
say "" printf '\n=== 1. Probe vehicle and archive ============================================\n'
say "=== 1. Probe vehicle and archive ============================================"
n=$(find "$FIXTURE" -type f 2>/dev/null | wc -l | tr -d ' ') n=$(find "$FIXTURE" -type f 2>/dev/null | wc -l | tr -d ' ')
if [ "$n" = "4" ]; then if [ "$n" = "4" ]; then
done_ "probe vehicle: 4 files under tests/fixtures/cowork-probe" done_ "probe vehicle: 4 files under tests/fixtures/cowork-probe"
@ -46,81 +53,103 @@ if [ -f "$ARCHIVE" ]; then
pend "archive: $ARCHIVE has $a entries and $leak leaked ones - rebuild it" pend "archive: $ARCHIVE has $a entries and $leak leaked ones - rebuild it"
fi fi
else else
pend "archive not built yet: $ARCHIVE" pend "archive not built: $ARCHIVE"
info "cd tests/fixtures/cowork-probe && zip -X $ARCHIVE .claude-plugin/plugin.json .mcp.json probe_tools.py skills/probe-versjon/SKILL.md"
fi fi
say "" printf '\n=== 2. Is jobbsok-probe installed in Cowork? =================================\n'
say "=== 2. Is jobbsok-probe installed in Cowork? =================================" # python3 does the walking: the path contains "Application Support", and an
# Bounded on purpose. A recursive content grep over the whole Claude support # unquoted $(find ...) in a for-loop splits on that space. That exact bug is
# directory walks caches and VM images and does not finish; the plugin store is # what made this check report "not installed" while the plugin was running.
# a known, shallow place, so look there by name and read only the small INSTALL_DIR=$(python3 - "$SESSIONS" "$PLUGIN_NAME" <<'PYEOF'
# manifests. A check that hangs is not a check. import json, os, sys
SESSIONS="$CLAUDE_SUPPORT/local-agent-mode-sessions" sessions, name = sys.argv[1], sys.argv[2]
hits=$(find "$SESSIONS" -maxdepth 6 -name '*jobbsok-probe*' 2>/dev/null | head -5) for dirpath, dirnames, filenames in os.walk(sessions):
for m in $(find "$SESSIONS" -maxdepth 4 \( -name 'manifest.json' -o -name 'cowork_settings.json' -o -name 'known_marketplaces.json' \) 2>/dev/null); do if os.path.basename(dirpath) != "rpm" or "manifest.json" not in filenames:
if grep -q 'jobbsok-probe' "$m" 2>/dev/null; then continue
hits="$hits try:
$m" data = json.load(open(os.path.join(dirpath, "manifest.json")))
fi except Exception:
done continue
hits=$(printf '%s\n' "$hits" | grep -v '^$') for p in data.get("plugins", []):
if [ -n "$hits" ]; then if p.get("name") == name:
done_ "Cowork store mentions jobbsok-probe:" print("%s\t%s\t%s\t%s" % (
printf '%s\n' "$hits" | sed 's|^| |' os.path.join(dirpath, p["id"]), p["id"],
else p.get("marketplaceName", "?"), p.get("updatedAt", "?")))
pend "no trace of jobbsok-probe in Cowork's store yet" sys.exit(0)
info "Upload the archive, or drag it into the Cowork chat and accept it." PYEOF
fi )
if [ -n "$INSTALL_DIR" ]; then
say "" D=$(printf '%s' "$INSTALL_DIR" | cut -f1)
say "=== 3. Did Cowork spawn the probe-tools server on THIS Mac? ==================" ID=$(printf '%s' "$INSTALL_DIR" | cut -f2)
say " (this is the -Host-MCP: question, measured rather than recalled)" MP=$(printf '%s' "$INSTALL_DIR" | cut -f3)
if [ -f "$MCP_LOG" ]; then AT=$(printf '%s' "$INSTALL_DIR" | cut -f4)
done_ "log exists: $MCP_LOG" done_ "listed in rpm/manifest.json as $ID (marketplace: $MP, updated $AT)"
info "last written: $(stat -f '%Sm' "$MCP_LOG" 2>/dev/null)" f=$(find "$D" -type f 2>/dev/null | wc -l | tr -d ' ')
for m in initialize tools/list tools/call; do if [ "$f" = "4" ]; then
if grep -q "method=\"$m\"" "$MCP_LOG" 2>/dev/null; then done_ "installed copy holds 4 files, matching the archive"
done_ "handshake: $m seen"
else else
pend "handshake: $m NOT seen in the log" pend "installed copy holds $f files, expected 4"
fi fi
done v=$(python3 -c "import json,sys;print(json.load(open(sys.argv[1]))['version'])" "$D/.claude-plugin/plugin.json" 2>/dev/null)
started=$(grep -h '\[probe-tools\] started on' "$MCP_LOG" 2>/dev/null | tail -1) if [ "$v" = "0.0.1" ]; then
if [ -n "$started" ]; then done_ "installed manifest version is 0.0.1 - not a stale cached build"
done_ "server's own stderr: $started"
else else
pend "the server never printed its start line - it may have failed to launch" pend "installed manifest version is '$v', expected 0.0.1 - Cowork served a cached build"
fi fi
else else
pend "no log at $MCP_LOG" pend "no plugin named $PLUGIN_NAME in any rpm/manifest.json"
info "Claude names each server's log mcp-server-<name>.log. No file means"
info "Cowork never even attempted to start probe-tools."
fi
if pgrep -f probe_tools.py >/dev/null 2>&1; then
done_ "process alive right now: $(pgrep -fl probe_tools.py | head -1 | cut -c1-100)"
else
info "no probe_tools.py process running at this instant (it may be started"
info "on demand, so this alone is not a nei - the log above is the record)"
fi fi
say "" printf '\n=== 3. Did Cowork spawn probe-tools on THIS Mac? =============================\n'
say "=== 4. Are the four answers written down? ===================================" printf ' (the -Host-MCP: question, measured rather than recalled)\n'
if [ -f "$MAIN_LOG" ]; then
conn=$(grep -a "Connected to $SERVER_KEY" "$MAIN_LOG" 2>/dev/null | tail -1)
neg=$(grep -a "$SERVER_KEY negotiated protocol version" "$MAIN_LOG" 2>/dev/null | tail -1)
if [ -n "$conn" ]; then
done_ "connected: $(printf '%s' "$conn" | sed 's/.*\[LocalMcpServerManager\] //')"
else
pend "main.log has no 'Connected to $SERVER_KEY' line"
fi
if [ -n "$neg" ]; then
done_ "handshake: $(printf '%s' "$neg" | sed 's/.*negotiated/negotiated/')"
else
pend "no protocol negotiation recorded"
fi
call=$(grep -a 'probe_ping' "$MAIN_LOG" 2>/dev/null | tail -1)
if [ -n "$call" ]; then
done_ "probe_ping seen in the log"
else
info "no probe_ping line in main.log yet - the connection lifecycle is"
info "logged there but an individual tool call may not be. Confirm the"
info "call by its answer in the Cowork chat, not by this line's absence."
fi
else
pend "no main.log at $MAIN_LOG"
fi
proc=$(pgrep -fl probe_tools.py 2>/dev/null | head -1)
if [ -n "$proc" ]; then
done_ "process alive: pid $(printf '%s' "$proc" | awk '{print $1}')"
interp=$(printf '%s' "$proc" | grep -oE '/[^ ]*python3?' | head -1)
[ -n "$interp" ] && info "interpreter: $interp (host, not sandbox)"
else
info "no probe_tools.py process at this instant - servers may be started on"
info "demand, so absence here is not a nei. The log above is the record."
fi
printf '\n=== 4. Are the four answers written down? ===================================\n'
RE='^- (Sesjonsmodus|Host-MCP|python3|CLAUDE_PLUGIN_ROOT): +(lokal VM|sky|ja|nei)\b' RE='^- (Sesjonsmodus|Host-MCP|python3|CLAUDE_PLUGIN_ROOT): +(lokal VM|sky|ja|nei)\b'
c=$(grep -cE "$RE" "$PROBE_DOC" 2>/dev/null) c=$(grep -cE "$RE" "$PROBE_DOC" 2>/dev/null)
if [ "$c" = "4" ]; then if [ "$c" = "4" ]; then
done_ "all four answered - plan Step 1 Verify passes" done_ "all four answered - plan Step 1 Verify passes"
grep -E '^- (Sesjonsmodus|Host-MCP|python3|CLAUDE_PLUGIN_ROOT):' "$PROBE_DOC" | sed 's|^| |'
else else
pend "$c of 4 answered in docs/cowork-probe.md" pend "$c of 4 answered in docs/cowork-probe.md"
grep -E '^- (Sesjonsmodus|Host-MCP|python3|CLAUDE_PLUGIN_ROOT):' "$PROBE_DOC" 2>/dev/null | sed 's|^| |'
fi fi
grep -E '^- (Sesjonsmodus|Host-MCP|python3|CLAUDE_PLUGIN_ROOT):' "$PROBE_DOC" 2>/dev/null | sed 's|^| |'
say "" printf '\n============================================================================\n'
say "============================================================================"
if [ "$pending" -eq 0 ]; then if [ "$pending" -eq 0 ]; then
say "ALL CHECKS LANDED." printf 'ALL CHECKS LANDED.\n'
exit 0 exit 0
fi fi
say "$pending check(s) still pending." printf '%s check(s) still pending.\n' "$pending"
exit 1 exit 1