jobbsok/scripts/cowork_probe_check.sh
Kjell Tore Guttormsen a4e6b44df4 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>
2026-09-04 18:51:45 +02:00

155 lines
6.6 KiB
Bash
Executable file

#!/bin/bash
# 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
# session. But the host-MCP one leaves hard traces on this Mac, and a trace is
# a measurement where "what did you see in the UI" is a recollection. This
# 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
# is pending. bash 3.2-clean, ASCII-only, read-only.
set -u
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
PROBE_DOC="$REPO_ROOT/docs/cowork-probe.md"
FIXTURE="$REPO_ROOT/tests/fixtures/cowork-probe"
ARCHIVE="${JOBBSOK_PROBE_ARCHIVE:-/tmp/jobbsok-probe.plugin}"
MAIN_LOG="$HOME/Library/Logs/Claude/main.log"
SESSIONS="$HOME/Library/Application Support/Claude/local-agent-mode-sessions"
PLUGIN_NAME="jobbsok-probe"
SERVER_KEY="plugin:jobbsok-probe:probe-tools"
pending=0
done_() { printf ' [DONE] %s\n' "$1"; }
pend() { printf ' [PENDING] %s\n' "$1"; pending=$((pending+1)); }
info() { printf ' %s\n' "$1"; }
printf '\n=== 1. Probe vehicle and archive ============================================\n'
n=$(find "$FIXTURE" -type f 2>/dev/null | wc -l | tr -d ' ')
if [ "$n" = "4" ]; then
done_ "probe vehicle: 4 files under tests/fixtures/cowork-probe"
else
pend "probe vehicle: expected 4 files, found $n"
fi
if [ -f "$ARCHIVE" ]; then
a=$(unzip -Z1 "$ARCHIVE" 2>/dev/null | wc -l | tr -d ' ')
leak=$(unzip -Z1 "$ARCHIVE" 2>/dev/null | grep -cE '^\.git|STATE\.md|\.venv|^\.claude/')
if [ "$a" = "4" ] && [ "$leak" = "0" ]; then
done_ "archive: $ARCHIVE ($a files, no leaked entries)"
else
pend "archive: $ARCHIVE has $a entries and $leak leaked ones - rebuild it"
fi
else
pend "archive not built: $ARCHIVE"
fi
printf '\n=== 2. Is jobbsok-probe installed in Cowork? =================================\n'
# python3 does the walking: the path contains "Application Support", and an
# unquoted $(find ...) in a for-loop splits on that space. That exact bug is
# what made this check report "not installed" while the plugin was running.
INSTALL_DIR=$(python3 - "$SESSIONS" "$PLUGIN_NAME" <<'PYEOF'
import json, os, sys
sessions, name = sys.argv[1], sys.argv[2]
for dirpath, dirnames, filenames in os.walk(sessions):
if os.path.basename(dirpath) != "rpm" or "manifest.json" not in filenames:
continue
try:
data = json.load(open(os.path.join(dirpath, "manifest.json")))
except Exception:
continue
for p in data.get("plugins", []):
if p.get("name") == name:
print("%s\t%s\t%s\t%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
pend "installed copy holds $f files, expected 4"
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
printf '\n=== 3. Did Cowork spawn probe-tools on THIS Mac? =============================\n'
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'
c=$(grep -cE "$RE" "$PROBE_DOC" 2>/dev/null)
if [ "$c" = "4" ]; then
done_ "all four answered - plan Step 1 Verify passes"
else
pend "$c of 4 answered in docs/cowork-probe.md"
fi
grep -E '^- (Sesjonsmodus|Host-MCP|python3|CLAUDE_PLUGIN_ROOT):' "$PROBE_DOC" 2>/dev/null | sed 's|^| |'
printf '\n============================================================================\n'
if [ "$pending" -eq 0 ]; then
printf 'ALL CHECKS LANDED.\n'
exit 0
fi
printf '%s check(s) still pending.\n' "$pending"
exit 1