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"],
p.get("marketplaceName", "?"), p.get("updatedAt", "?")))
sys.exit(0)
PYEOF
)
if [ -n "$INSTALL_DIR" ]; then
D=$(printf '%s' "$INSTALL_DIR" | cut -f1)
ID=$(printf '%s' "$INSTALL_DIR" | cut -f2)
MP=$(printf '%s' "$INSTALL_DIR" | cut -f3)
AT=$(printf '%s' "$INSTALL_DIR" | cut -f4)
done_ "listed in rpm/manifest.json as $ID (marketplace: $MP, updated $AT)"
f=$(find "$D" -type f 2>/dev/null | wc -l | tr -d ' ')
if [ "$f" = "4" ]; then
done_ "installed copy holds 4 files, matching the archive"
else else
pend "no trace of jobbsok-probe in Cowork's store yet" pend "installed copy holds $f files, expected 4"
info "Upload the archive, or drag it into the Cowork chat and accept it." fi
v=$(python3 -c "import json,sys;print(json.load(open(sys.argv[1]))['version'])" "$D/.claude-plugin/plugin.json" 2>/dev/null)
if [ "$v" = "0.0.1" ]; then
done_ "installed manifest version is 0.0.1 - not a stale cached build"
else
pend "installed manifest version is '$v', expected 0.0.1 - Cowork served a cached build"
fi
else
pend "no plugin named $PLUGIN_NAME in any rpm/manifest.json"
fi fi
say "" printf '\n=== 3. Did Cowork spawn probe-tools on THIS Mac? =============================\n'
say "=== 3. Did Cowork spawn the probe-tools server on THIS Mac? ==================" printf ' (the -Host-MCP: question, measured rather than recalled)\n'
say " (this is the -Host-MCP: question, measured rather than recalled)" if [ -f "$MAIN_LOG" ]; then
if [ -f "$MCP_LOG" ]; then conn=$(grep -a "Connected to $SERVER_KEY" "$MAIN_LOG" 2>/dev/null | tail -1)
done_ "log exists: $MCP_LOG" neg=$(grep -a "$SERVER_KEY negotiated protocol version" "$MAIN_LOG" 2>/dev/null | tail -1)
info "last written: $(stat -f '%Sm' "$MCP_LOG" 2>/dev/null)" if [ -n "$conn" ]; then
for m in initialize tools/list tools/call; do done_ "connected: $(printf '%s' "$conn" | sed 's/.*\[LocalMcpServerManager\] //')"
if grep -q "method=\"$m\"" "$MCP_LOG" 2>/dev/null; then
done_ "handshake: $m seen"
else else
pend "handshake: $m NOT seen in the log" pend "main.log has no 'Connected to $SERVER_KEY' line"
fi fi
done if [ -n "$neg" ]; then
started=$(grep -h '\[probe-tools\] started on' "$MCP_LOG" 2>/dev/null | tail -1) done_ "handshake: $(printf '%s' "$neg" | sed 's/.*negotiated/negotiated/')"
if [ -n "$started" ]; then
done_ "server's own stderr: $started"
else else
pend "the server never printed its start line - it may have failed to launch" 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 fi
else else
pend "no log at $MCP_LOG" pend "no main.log at $MAIN_LOG"
info "Claude names each server's log mcp-server-<name>.log. No file means"
info "Cowork never even attempted to start probe-tools."
fi fi
if pgrep -f probe_tools.py >/dev/null 2>&1; then proc=$(pgrep -fl probe_tools.py 2>/dev/null | head -1)
done_ "process alive right now: $(pgrep -fl probe_tools.py | head -1 | cut -c1-100)" 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 else
info "no probe_tools.py process running at this instant (it may be started" info "no probe_tools.py process at this instant - servers may be started on"
info "on demand, so this alone is not a nei - the log above is the record)" info "demand, so absence here is not a nei. The log above is the record."
fi fi
say "" printf '\n=== 4. Are the four answers written down? ===================================\n'
say "=== 4. Are the four answers written down? ==================================="
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