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:
parent
e1ad1b688f
commit
a4e6b44df4
1 changed files with 98 additions and 69 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue