jobbsok/docs/cowork-probe.md
Kjell Tore Guttormsen 41854ae01a docs(m1): record three probe findings outside the four gated bullets
Recorded here rather than as extra bullets: the four are gated on a count of
exactly four and Step 31 adds a fifth, so a sixth would break the Verify.
Checked both ways -- the live file still counts 0, a fully answered copy still
counts exactly 4.

O1. Host-side MCP reaches a cloud session through localMcpBridge. research/03
    dimension 4 quoted Anthropic saying local MCP servers do not run in cloud
    sessions, and flagged as unverified whether they still spawn on this Mac.
    They do: main.log records Connecting / negotiated 2025-11-25 / Connected
    (1 tools) / announcing, and ps shows the server as a child of Claude.app on
    the host Python 3.14. The session addresses it as
    mcp__remote-devices__plugin_jobbsok-probe_probe-tools__probe_ping. So
    "host-side server" and "cloud session" are not exclusive here.

O2. Sandbox and host are two different interpreters: 3.11.15 [GCC 13.3.0] Linux
    inside, 3.14.0 macOS outside, with CLAUDE_PLUGIN_ROOT resolving under
    /root/.claude/plugins/synced/. That is also what answers the session-mode
    question -- measured from the paths, not read off a settings screen. It
    matters for M3: guard_ingest.py cannot import the guard in the sandbox, so
    it belongs on the host side of the bridge that O1 shows is reachable.

O3. Cowork's built-in browser rendered a Finn job ad in full, with the operator
    logged in after signing in by hand inside that browser. That is the brief's
    invariant by a shorter route -- no credential handling, manual auth, an
    already-authenticated browser. It raises an M3 question about what
    Playwright over CDP still adds; the question is recorded, not decided.
    Two limits are stated rather than glossed: login persistence across
    sessions was not measured, and the built-in browser leaves no host-side
    trace, so unlike O1 and O2 the operator's observation is the only
    instrument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 19:02:06 +02:00

345 lines
17 KiB
Markdown

# Cowork probe
Plan Step 1 (M1). An empirical record of what this operator's Cowork
installation actually does, measured before any code depends on it.
Operator decision 7 — Python as a CLI plus a host-side stdio MCP server — is
void if host-side servers never appear in a Cowork session. That is not
knowable from Claude Code, so it is measured here first, and the milestone that
depends on it carries a written degradation branch either way.
**Status: unanswered.** All four questions require the operator sitting in a
Cowork session on this Mac. The executor session built the probe vehicle and
this checklist; it did not and could not answer them. The verify command for
this step is written so an unanswered checklist *fails*:
```bash
test "$(grep -cE '^- (Sesjonsmodus|Host-MCP|python3|CLAUDE_PLUGIN_ROOT): +(lokal VM|sky|ja|nei)\b' docs/cowork-probe.md)" = 4
```
A bullet still reading `ubesvart` does not match, the count drops below four,
and the command exits non-zero. That is the intended behaviour until the
operator has run the probe.
## Results
Overwrite `ubesvart` with one of the allowed answers, and put the date of the
measurement in the Measured column. A `nei` is a result, not a failure — it
selects the degradation branch below.
- Sesjonsmodus: ubesvart
- Host-MCP: ubesvart
- python3: ubesvart
- CLAUDE_PLUGIN_ROOT: ubesvart
| Bullet | What the operator does in Cowork | Allowed answer | Measured |
|---|---|---|---|
| `- Sesjonsmodus:` | Open a Cowork session and read whether it is a local VM or cloud | `lokal VM` or `sky` | |
| `- Host-MCP:` | Upload `jobbsok-probe.plugin` via Customize -> Plugins; does `probe-tools` appear as a connector, and does `probe_ping` return? | `ja` or `nei` | |
| `- python3:` | Run the `probe-versjon` skill; does it succeed in invoking `python3`, and which version does it report? | `ja` or `nei`, followed by the reported version | |
| `- CLAUDE_PLUGIN_ROOT:` | Run `probe-versjon`; does it print a resolved absolute path, or the literal variable name? | `ja` or `nei` | |
### Two-run record: is the session-location setting the lever?
Cowork exposes `Only on this computer` as a **toggle**, which means session mode
is a setting rather than a property of this machine -- something the four
bullets above cannot express, since each holds one value. Anthropic's
documentation says local MCP servers do not run in cloud sessions, and cloud has
been the default since 2026-07-07; whether a cloud-default session on this Mac
still spawns host-side servers via the desktop app is recorded as **unverified**
in `research/03-cowork-plugin-compatibility.md`, dimension 4. So run the probe
twice and fill this in **before** writing the bullets above. The off run is the
cheap one to lose: it is the current default, and a fresh install returns to it.
| Run | `Only on this computer` | `probe-tools` listed? | `probe_ping` answers? | Manifest version | Date |
|---|---|---|---|---|---|
| 1 | off (current default) | | | | |
| 2 | on | | | | |
How to read the pair:
| Run 1 (off) | Run 2 (on) | What it means |
|---|---|---|
| no | yes | The toggle is the lever. Operator decision 7 works, but only with it on -- which then belongs in `README.md` and in every `Verifiser i Cowork` step, not in a footnote. |
| no | no | Decision 7's Cowork half is dead whatever the setting. The degradation branch below applies, and `jobbsok-tools` is not built on an assumption. |
| yes | yes | Host-side MCP runs even in cloud mode. That contradicts the documentation and is the most interesting of the three -- research 03 flagged exactly this as unverified. Record what you saw, verbatim. |
Then write the session-mode bullet above as the mode that produced the better
result, and put what the other run gave in its Measured column. If both runs
agree, say so there too: an agreement that was measured reads differently from
one that was assumed.
### What is already known, and what is not
Measured 2026-09-04 from Claude Code, before the probe was built:
- Cowork is installed and available on this Intel Mac.
- The **session mode is not readable from disk.** `cowork_settings.json` carries
only `extraKnownMarketplaces`; the desktop config carries `coworkNetworkMode`
and a trusted-folder list, and neither states where a session runs. So
`- Sesjonsmodus:` cannot be answered by reading files — it has to be read off
a live session.
- The other three each require a plugin installed in a live Cowork session.
Nothing about them is decidable from Claude Code.
## The probe vehicle
`jobbsok-probe` — a throwaway test plugin, deliberately **not** the `jobbsok`
plugin itself. `jobbsok`'s own `.mcp.json` (Step 12) and packaging script
(Step 13) land eleven steps later, and this probe must not wait for them.
It holds exactly four files, under `tests/fixtures/cowork-probe/`:
| File | What it is for |
|---|---|
| `.claude-plugin/plugin.json` | Manifest. Name `jobbsok-probe`, version `0.0.1`. The version is what `probe-versjon` echoes, so a mismatch reveals a cached build. |
| `skills/probe-versjon/SKILL.md` | A skill that reports the manifest version, the resolved `${CLAUDE_PLUGIN_ROOT}`, and whether it can invoke `python3`. Answers three of the four questions. |
| `.mcp.json` | Declares one stdio server, `probe-tools`. Its appearance in the session is the whole `- Host-MCP:` question. |
| `probe_tools.py` | Standard-library JSON-RPC server exposing one tool, `probe_ping`, returning the interpreter actually running it. No third-party imports: a server that failed on a missing dependency would answer a different question than the one asked. |
Verified from Claude Code 2026-09-04: the server completes an `initialize`
handshake, lists `probe_ping`, returns an interpreter version on
`tools/call`, and rejects an unknown tool name. What is unverified is whether
Cowork ever starts it.
### Building the archive
Run from the repository root. The file list is explicit, and that is the point:
```bash
cd tests/fixtures/cowork-probe && \
zip -X /tmp/jobbsok-probe.plugin \
.claude-plugin/plugin.json \
.mcp.json \
probe_tools.py \
skills/probe-versjon/SKILL.md
```
Never `zip -r` over the repository root. A recursive archive of the repo would
ship `.git`, `STATE.md`, `.claude/` and the virtualenv into an uploaded plugin.
The include list above names the four files and can carry nothing else.
## Running the probe, step by step
Everything below happens in Claude Cowork on this Mac, not in Claude Code.
Where a UI path is named it comes from Anthropic's own documentation
(`research/03-cowork-plugin-compatibility.md`, dimensions 1 and 6); where the
wording in the app turns out to differ, trust the app and note what you saw --
a changed menu label is itself a finding worth recording.
### A. Read the session mode -> answers `- Sesjonsmodus:`
Open Cowork and find its session-location setting (Cowork -> Settings). You are
looking for whether sessions run **on this machine** or **in the cloud**.
- On this machine: shell commands execute in a dedicated local Linux VM
(Apple Virtualization.framework), which needs working hardware
virtualization; Claude runs a readiness check for exactly that.
- In the cloud: code runs on Anthropic's servers. This has been the default
since 2026-07-07, so do not assume local just because the Mac is capable.
Write `lokal VM` or `sky`.
**Do not just read this setting -- probe both sides of it.** It is a toggle, so
the documented claim that local MCP servers do not run in cloud sessions is
testable here rather than merely quotable. Leave it as you found it for the
first pass, run sections B through E, then flip it, start a fresh session and
run B through E again. Record both in "Two-run record" above; that table also
says how to read the pair. Five minutes buys the difference between "Cowork
cannot do this" and "Cowork can do this if one setting is on", which are very
different findings for the milestone.
One honesty marker: that this toggle controls session location is an inference
from its own label and subtitle -- a local VM stops when the machine sleeps, a
cloud session does not -- and no documentation read so far names this exact
string. The probe is what settles it.
### B. Install the probe plugin (needed for C, D and E)
Two documented routes; either is fine.
1. Customize -> Plugins, choose the install-from-file / upload option, and pick
`/tmp/jobbsok-probe.plugin`.
2. Or drag the `.plugin` file into the Cowork chat. Anthropic's own plugin
packaging skill says a `.plugin` file appears in the chat as a rich preview
with a button to accept it.
Known wrinkle, not your mistake if it happens: uploaded plugins have been
reported not to survive an app relaunch (claude-code#65426, June 2026). If the
plugin disappears later, re-upload it rather than concluding anything about
the probe.
### C. Confirm you are testing the build you just uploaded
In the Cowork chat, type `/` (or `+`) to list available skills, and run
`probe-versjon`.
The first thing it reports is the manifest version. It must be **`0.0.1`**.
If it reports anything else, Cowork served a cached build rather than the one
you uploaded (claude-code#69020, open) -- and then every answer below is about
some *other* build. Uninstall, re-upload, and re-run before continuing. This
check exists because a stale cache produces confident, wrong measurements.
### D. Read the skill's output -> answers `- python3:` and `- CLAUDE_PLUGIN_ROOT:`
Both come from that same `probe-versjon` run.
**`- python3:`** -- did the skill succeed in running
`python3 -c 'import sys; print(sys.version)'`, and what did it print?
Write `ja` followed by the version, or `nei` followed by the error text.
One thing to be precise about, because it changes what the answer means: in
Cowork, shell commands run inside the sandbox (the local VM, or the cloud
environment), **not on your Mac**. So this measures the sandbox's `python3`,
not the host's. That is the right thing to measure here -- it tells us whether
a skill-invoked script can run in Cowork at all -- but it is not evidence about
the interpreter that `scripts/bootstrap.sh` just built a virtualenv for.
**`- CLAUDE_PLUGIN_ROOT:`** -- did the skill print a resolved absolute path, or
the literal unexpanded string `${CLAUDE_PLUGIN_ROOT}`? Write `ja` or `nei`.
An absolute path is the pass; anything literal or empty is `nei`.
### E. Look for the connector -> answers `- Host-MCP:`
This is the load-bearing one: it is the entire Cowork half of operator
decision 7, and the reason this probe is the first step of the build.
A server declared in `.mcp.json` does **not** show up as a skill. In Cowork it
appears as a **Connector**. Two checks, and both must hold:
1. **Is it listed?** Customize -> Plugins, open `jobbsok-probe`, and look at
its connectors page. Is `probe-tools` there?
2. **Does it answer?** In the chat, ask Claude to call the `probe_ping` tool.
It should return JSON containing `python_version` and `python_executable`.
Write `ja` only if both hold. If `probe-tools` is listed but never answers,
write `nei` and note that it appeared but did not respond -- a connector that
does not respond is not a working host-side server, and the difference between
"absent" and "present but mute" points at different causes.
Unlike D, this one *does* run on your Mac: Anthropic moved local MCP servers
out of the sandbox in May 2026 and runs them as ordinary host software. So a
`ja` here means the host Python, the host filesystem and `~/jobbsok-workspace`
are all reachable from Cowork -- which is what decision 7 is built on.
### F. Record and verify
Edit the four bullets under "Results" above, replacing `ubesvart`, and fill in
the Measured column with today's date. Then, from the repository root:
```bash
test "$(grep -cE '^- (Sesjonsmodus|Host-MCP|python3|CLAUDE_PLUGIN_ROOT): +(lokal VM|sky|ja|nei)\b' docs/cowork-probe.md)" = 4
```
Exit 0 means the probe is complete and plan Step 1 can be closed. Exit 1 means
at least one bullet is still unanswered or was written in a form the grep does
not recognise -- check that the answer follows the label directly, with a
space, and starts with one of the four allowed words.
## Observations beyond the four questions
Measured 2026-09-04 while running the probe. These are deliberately **not**
extra bullets: the four above are gated on a count of exactly four, and Step 31
already adds a fifth (`- Connector-hook:`). A sixth would break the Verify. They
are recorded here because they answer questions later milestones were going to
ask, and because a finding that lives only in a commit message is lost.
### O1. Host-side MCP reaches a cloud session, through a bridge
The strongest result of the probe, and it contradicts the reading this plan was
built on. `research/03` dimension 4 quotes Anthropic: local MCP servers "don't
run in sessions in the cloud", and flagged as **unverified** whether a
cloud-default session on this Mac still spawns them. It does.
Measured from the host, not recalled from a UI:
```
main.log 2026-09-04 18:48:28
[LocalMcpServerManager] Connecting to plugin:jobbsok-probe:probe-tools
[LocalMcpServerManager] ... negotiated protocol version: 2025-11-25
[LocalMcpServerManager] Connected to plugin:jobbsok-probe:probe-tools (1 tools)
[localMcpBridge] announcing plugin:jobbsok-probe:probe-tools: 1 tool(s)
ps: 12997 Claude.app/Contents/Helpers/disclaimer -- /usr/local/bin/python3
.../rpm/plugin_01NxHfqM495jtJzjVjRvgWDm/probe_tools.py
12998 Python.framework/Versions/3.14/... (the host interpreter)
```
The mechanism is `localMcpBridge`, the same one that announces Claude Desktop's
own `Filesystem` server. The session sees the tool as
`mcp__remote-devices__plugin_jobbsok-probe_probe-tools__probe_ping`. So
"host-side server" and "cloud session" are not exclusive here: the server runs
on the Mac and is bridged in.
One design choice was vindicated by the log. `probe_tools.py` echoes the
client's `protocolVersion` rather than asserting its own default. The client
asked for `2025-11-25`, which is newer than the server's fallback; a hardcoded
version would have made this handshake a different and worse story.
### O2. The sandbox and the host are two different interpreters
| Where | Interpreter | Evidence |
|---|---|---|
| Cowork sandbox (skills, Bash) | Python 3.11.15, `[GCC 13.3.0]`, Linux | reported by `probe-versjon` |
| Host (MCP servers) | Python 3.14.0, macOS framework build | `ps`, and the venv Step 2 built |
`${CLAUDE_PLUGIN_ROOT}` resolves inside the sandbox to
`/root/.claude/plugins/synced/<session>_<account>/jobbsok-probe`. Both facts
matter for M3: `guard_ingest.py` cannot import the guard in the sandbox, so it
belongs on the host side of the bridge, which O1 shows is reachable.
The Linux paths and the GCC build string are also what answer the session-mode
question. It was read off a measurement, not off a settings screen.
### O3. Cowork's built-in browser reads Finn, authenticated
Operator measurement, 2026-09-04. Cowork's built-in browser (shipped
2026-08-26) rendered `www.finn.no/job/ad/474745458` in full -- employer, title,
`Soknadsfrist`, `Mulighet for hjemmekontor`, `Arbeidsspraak`, `Sektor`: the
exact fields the M3 extraction wants, at a clean snapshot-able URL. The
operator was logged in to Finn, having signed in by hand inside that browser.
This is the brief's invariant reached by a shorter route: the plugin handles no
credentials, the operator authenticates manually, and the browser is already
authenticated when it reads. It raises a real M3 question -- if the built-in
browser covers both public and logged-in sources, what does Playwright MCP over
CDP still add? -- but that is an M3 decision and is not taken here.
Not established, and worth knowing before the decision is taken: whether that
login persists across sessions or must be repeated each time. Nothing was
measured about persistence.
Unlike O1 and O2, this one has no host-side trace: the built-in browser writes
nothing to `main.log` (checked for `finn.no`, `webview`, `browserTool` --
no hits). The operator's observation is the only instrument.
## Degradation branch
Written now, so the answer does not get decided under pressure later.
**If `- Host-MCP:` is `nei`** — host-side stdio servers do not appear in this
Cowork installation:
1. Operator decision 7's Cowork half is unavailable. Nothing changes for Claude
Code, where the scripts are invoked directly.
2. Every script-backed skill degrades to reading files that the Claude Code CLI
produced. The skills stay useful; they stop being able to compute.
3. `Verifiser i Cowork` for the affected milestones means "the skills load and
read files produced from Claude Code" — not "the connector answers".
4. The degradation is recorded in `STATE.md`.
This is risk H12's mitigation, and it is why the probe is the first step of the
build rather than a later check.
**If `- python3:` is `nei`** — a skill cannot invoke `python3` in a Cowork
session: the same degradation applies, and more broadly, since it removes the
CLI half too. Escalate to the operator before building further on decision 7.
**If `- CLAUDE_PLUGIN_ROOT:` is `nei`** — the variable does not resolve: every
intra-plugin path in the plan is affected, not just this milestone's. Escalate;
do not paper over it with hardcoded paths.
**If `- Sesjonsmodus:` is `sky`** — sessions run in the cloud, and host-side
servers cannot reach this machine at all. `- Host-MCP:` should then read `nei`,
and its branch applies. If the two disagree, the disagreement is the finding.