From b15135f8b0e90315d94b0a4509bc40f2c59bdede Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Mon, 10 Aug 2026 18:21:51 +0200 Subject: [PATCH 1/3] Specify Claude native DING transport --- docs/vrs/01-ding/01-claude/spec.md | 124 ++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 4 deletions(-) diff --git a/docs/vrs/01-ding/01-claude/spec.md b/docs/vrs/01-ding/01-claude/spec.md index 4427d367..3d7abd1d 100644 --- a/docs/vrs/01-ding/01-claude/spec.md +++ b/docs/vrs/01-ding/01-claude/spec.md @@ -1,12 +1,128 @@ # Claude harness specification -The screen grammar by which DING recognizes a Claude composer. It realizes -[`../requirements.md`](../requirements.md) through the mechanism in -[`../spec.md`](../spec.md). +This document defines native MCP delivery and the legacy Claude screen +grammar. It realizes [`../requirements.md`](../requirements.md) through the +selection and durability rules in [`../spec.md`](../spec.md). ## Status -Active. +Active. Native production acceptance is blocked until the provider approves a +non-interactive channel enrollment for this integration. + +## Native MCP transport + +`deliver "mcp"` selects this transport. st2 starts one `st2 claude-mcp` +process for the declared agent. The process serves MCP over standard input and +standard output. It does not use the PTY, inspect the screen, or start the +legacy `ding` sidecar. + +### Provider enablement + +The launched Claude session must enable the MCP server as a channel source. +An MCP connection alone is not sufficient. Claude can connect to a server, +list its tools, and still discard every channel notification when the server +is not enabled as a channel. + +The server advertises the `experimental.claude/channel` capability. The st2 +launch configuration must select that server as a channel. The MCP wire does +not return a channel-acceptance receipt to the server, so the adapter must not +claim that it detects provider acceptance at runtime. The provider evidence +below proves that the maintained launch configuration accepts the channel. + +The maintained launch path must use a provider-approved channel enrollment. +The provider's development-only channel override is observation and test +equipment. It is not production acceptance. + +### Notification + +The adapter sends one MCP notification with this method: + +```text +notifications/claude/channel +``` + +The notification parameters have this shape: + +```json +{ + "content": "Subject: \n\n", + "meta": { + "from": "", + "messageFilename": "", + "threadFilename": "", + "identity": "" + } +} +``` + +`content` is the message body. A present subject adds the shown `Subject:` +line and one blank line. A message without a subject uses the body alone. +`threadFilename` is the valid `in-reply-to` value when one exists. Otherwise, +it is `messageFilename`. The adapter does not invent sender, thread, or +recipient values from display text. + +Claude presents an accepted notification as an inbound channel item. When the +session is idle, the item starts a turn without terminal input from st2. This +provider behavior is the native wake receipt. The adapter does not parse the +rendered inbound line and does not wait for a model response. + +### Durable inbox and retry + +The selected catalog inbox remains authoritative. The adapter does not archive +or delete a message after notification. The agent reads and archives the +message through the normal message commands. + +Before each notification attempt, the adapter runs the normal message sweep. +An archive record with the same filename wins. The adapter sends nothing for a +message that the sweep removes from the inbox. + +The notification attempt succeeds only when the MCP transport is open and the +notification write completes. A successful write completes this DING attempt; +it does not acknowledge, read, or archive the message. A write failure keeps +the message eligible for retry. A closed transport is a normal adapter stop. +It sends nothing further and leaves every remaining message unread. + +The adapter scans the existing inbox when it starts. It then uses a file +watcher as a low-latency signal and a poll as the correctness backstop. The +poll interval is at most 15 seconds. Watcher and poll observations share one +successful-delivery set, so they do not send the same file twice in one +adapter incarnation. A failed notification does not enter that set. + +The poll path must work when the watcher is disabled. This is a required test +mode and a production recovery property. The watcher may fail or miss an +event without stopping delivery. + +One malformed or unreadable inbox file must not stop the scan, watcher, poll, +or later valid messages. The adapter reports the file failure, leaves that +file unread, and continues with the other files. + +### Shutdown and recovery + +Transport close stops the adapter without an error. st2 may start a new +adapter while the agent still declares this transport. A new adapter scans the +inbox again. It may repeat a notification that a prior adapter wrote before it +lost its process-local delivery set. This at-least-once behavior is safe +because the inbox file is still the source of truth and message actions are +idempotent. + +### Provider evidence + +A live Claude Code observation must prove all of these results before the +native adapter is accepted: + +- Claude reports the MCP server as connected and the channel as accepted. +- A message sent to an idle agent appears as an inbound channel item. +- The inbound item starts observable work without a user keystroke. +- A message sent during active work does not corrupt user input or terminal + state. +- The inbox file remains until the agent reads and archives it. +- A deliberately broken notification or disabled transport makes the test + fail. + +## Legacy screen transport + +The remaining sections define the unchanged screen grammar selected by +`ding`. ## Locating the composer From 22e72df7e42f180439df30b554c6a03891ad5c75 Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Mon, 10 Aug 2026 18:24:01 +0200 Subject: [PATCH 2/3] Document Claude channel approval boundary --- docs/vrs/01-ding/01-claude/packaging.md | 82 +++++++++++++++++++++++++ docs/vrs/01-ding/01-claude/spec.md | 8 ++- 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 docs/vrs/01-ding/01-claude/packaging.md diff --git a/docs/vrs/01-ding/01-claude/packaging.md b/docs/vrs/01-ding/01-claude/packaging.md new file mode 100644 index 00000000..003dc701 --- /dev/null +++ b/docs/vrs/01-ding/01-claude/packaging.md @@ -0,0 +1,82 @@ +# Claude channel packaging boundary + +This page defines the work needed to make `st2 claude-mcp` an approved Claude +Code channel. It is a decision aid, not build authority. + +## Plugin shape + +The plugin source would live at `integrations/claude-channel/` in this +repository. Its root would contain two files: + +```text +integrations/claude-channel/ +├── .claude-plugin/plugin.json +└── .mcp.json +``` + +`.claude-plugin/plugin.json` would declare the stable plugin name and one +`channels` entry. The entry's `server` value would name the MCP server in +`.mcp.json`. `.mcp.json` would declare one standard-input MCP server whose +command is `st2` and whose arguments are `claude-mcp`. The installed st2 binary +would remain the implementation. The plugin would contain only provider +metadata and launch configuration. + +Claude Code requires the channel server name in the manifest to match the MCP +server key. The plugin must pass `claude plugin validate --strict` before +distribution. See the provider's +[plugin reference](https://code.claude.com/docs/en/plugins-reference#channels). + +## Private-fleet distribution + +A private marketplace is a versioned catalog that points Claude Code at the +plugin source. Fleet setup would register that marketplace, install the plugin +at user or managed scope, and start each Claude agent with this selector: + +```text +--channels plugin:st2-channel@ +``` + +The selector is required for every session. Installation alone does not enable +the channel. A private marketplace also does not approve the channel during +the provider's research preview. + +## Organization policy + +An eligible organization can approve its private plugin without Anthropic +marketplace review. An Owner must enable Claude Code channels and set managed +policy like this: + +```json +{ + "channelsEnabled": true, + "allowedChannelPlugins": [ + { "marketplace": "", "plugin": "st2-channel" } + ] +} +``` + +`allowedChannelPlugins` replaces the provider's default allowlist. The policy +must therefore include every channel plugin that the organization still wants +to permit. Users cannot override it. + +This control is available to claude.ai Team and Enterprise organizations and +to qualifying Console organizations that deploy managed settings. Enabling it +in the claude.ai Admin console requires the Owner role. A personal Pro or Max +subscription cannot add a private plugin to this organization allowlist. Under +that subscription, the choices are Anthropic marketplace approval or a move to +an eligible organization and authentication path. + +The provider documents these rules under +[Enterprise controls](https://code.claude.com/docs/en/channels#enterprise-controls) +and +[Research preview](https://code.claude.com/docs/en/channels#research-preview). + +## Current acceptance boundary + +The development-only override asks for interactive confirmation on every +Claude process start. It cannot support an unattended st2 agent. st2 must not +emit that flag in a maintained launcher. + +The MCP server, watcher, poll backstop, and durable inbox boundary can be built +and tested without this package. Production launch acceptance waits for one of +the approved paths above. diff --git a/docs/vrs/01-ding/01-claude/spec.md b/docs/vrs/01-ding/01-claude/spec.md index 3d7abd1d..53e1e2b0 100644 --- a/docs/vrs/01-ding/01-claude/spec.md +++ b/docs/vrs/01-ding/01-claude/spec.md @@ -6,8 +6,8 @@ selection and durability rules in [`../spec.md`](../spec.md). ## Status -Active. Native production acceptance is blocked until the provider approves a -non-interactive channel enrollment for this integration. +Active. Native production acceptance is blocked until provider or organization +policy supplies a non-interactive channel enrollment for this integration. ## Native MCP transport @@ -33,6 +33,10 @@ The maintained launch path must use a provider-approved channel enrollment. The provider's development-only channel override is observation and test equipment. It is not production acceptance. +The bounded packaging and policy path is in +[`packaging.md`](./packaging.md). That document does not authorize building the +plugin. + ### Notification The adapter sends one MCP notification with this method: From c102904a7f8a26561a54c380e8aef1e403f7c55c Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Tue, 11 Aug 2026 07:08:12 +0200 Subject: [PATCH 3/3] docs: record measured Claude channel delivery --- docs/vrs/01-ding/01-claude/packaging.md | 54 +++++--- docs/vrs/01-ding/01-claude/spec.md | 90 ++++++++++--- tools/channel-probe/README.md | 30 +++++ tools/channel-probe/probe-server.js | 57 ++++++++ tools/channel-probe/run-probe.sh | 170 ++++++++++++++++++++++++ 5 files changed, 364 insertions(+), 37 deletions(-) create mode 100644 tools/channel-probe/README.md create mode 100644 tools/channel-probe/probe-server.js create mode 100755 tools/channel-probe/run-probe.sh diff --git a/docs/vrs/01-ding/01-claude/packaging.md b/docs/vrs/01-ding/01-claude/packaging.md index 003dc701..e0b90221 100644 --- a/docs/vrs/01-ding/01-claude/packaging.md +++ b/docs/vrs/01-ding/01-claude/packaging.md @@ -1,7 +1,20 @@ # Claude channel packaging boundary -This page defines the work needed to make `st2 claude-mcp` an approved Claude -Code channel. It is a decision aid, not build authority. +This page defines the optional work needed to make `st2 claude-mcp` a managed +Claude Code channel plugin. It is a decision aid, not build authority. + +## Decision + +Do not package the plugin yet. The maintained private-fleet path owns the st2 +binary and its startup PTY. It can accept the provider's exact direct-server +warning once per Claude process. This is bounded startup control, not screen +classification for each message. + +The plugin is worth building when a deployment requires zero-interaction +startup, a centrally managed allowlist, or distribution of the channel metadata +independently from st2. Until then it adds a marketplace, installation, and +organization-policy surface without changing the MCP transport or durable inbox +contract. ## Plugin shape @@ -37,8 +50,8 @@ at user or managed scope, and start each Claude agent with this selector: ``` The selector is required for every session. Installation alone does not enable -the channel. A private marketplace also does not approve the channel during -the provider's research preview. +the channel. The plugin manifest selects the MCP server named by its `channels` +entry; the command-line selector names the plugin and marketplace. ## Organization policy @@ -59,24 +72,31 @@ policy like this: must therefore include every channel plugin that the organization still wants to permit. Users cannot override it. -This control is available to claude.ai Team and Enterprise organizations and -to qualifying Console organizations that deploy managed settings. Enabling it -in the claude.ai Admin console requires the Owner role. A personal Pro or Max -subscription cannot add a private plugin to this organization allowlist. Under -that subscription, the choices are Anthropic marketplace approval or a move to -an eligible organization and authentication path. +This policy is needed only when the deployment uses managed settings. A host +with no managed policy can accept a locally declared server through the +development selector. A host whose managed policy disables channels rejects the +development selector too. The provider documents these rules under [Enterprise controls](https://code.claude.com/docs/en/channels#enterprise-controls) and [Research preview](https://code.claude.com/docs/en/channels#research-preview). -## Current acceptance boundary +## Direct-server acceptance boundary + +The current maintained path starts the locally built and pinned st2 MCP server +with this selector: + +```text +--dangerously-load-development-channels server: +``` -The development-only override asks for interactive confirmation on every -Claude process start. It cannot support an unattended st2 agent. st2 must not -emit that flag in a maintained launcher. +Claude asks for one confirmation on every process start. The launcher may send +Return only after it recognizes the exact warning and exact expected selector. +It must not answer a workspace trust question or another startup prompt on that +basis. After this startup gate, native DING uses only MCP notifications and no +terminal input. -The MCP server, watcher, poll backstop, and durable inbox boundary can be built -and tested without this package. Production launch acceptance waits for one of -the approved paths above. +This path is not permission to run arbitrary downloaded channel servers. If the +server is no longer a locally owned st2 artifact, or if the deployment requires +no startup confirmation, build and validate the plugin path above. diff --git a/docs/vrs/01-ding/01-claude/spec.md b/docs/vrs/01-ding/01-claude/spec.md index 53e1e2b0..16eacfd1 100644 --- a/docs/vrs/01-ding/01-claude/spec.md +++ b/docs/vrs/01-ding/01-claude/spec.md @@ -6,8 +6,10 @@ selection and durability rules in [`../spec.md`](../spec.md). ## Status -Active. Native production acceptance is blocked until provider or organization -policy supplies a non-interactive channel enrollment for this integration. +Active. Live Claude Code 2.1.226 and 2.1.227 observations prove idle wake, +active-turn delivery, and queued delivery through a directly declared MCP +server. The maintained private-fleet path uses one bounded provider confirmation +when the Claude process starts. It does not classify the screen for each DING. ## Native MCP transport @@ -18,24 +20,45 @@ legacy `ding` sidecar. ### Provider enablement -The launched Claude session must enable the MCP server as a channel source. -An MCP connection alone is not sufficient. Claude can connect to a server, -list its tools, and still discard every channel notification when the server -is not enabled as a channel. +The launched Claude session must enable the MCP server as a channel source. An +MCP connection alone is not sufficient. Claude can connect to a server, list +its tools, and still discard every channel notification when the server is not +enabled as a channel. -The server advertises the `experimental.claude/channel` capability. The st2 -launch configuration must select that server as a channel. The MCP wire does -not return a channel-acceptance receipt to the server, so the adapter must not -claim that it detects provider acceptance at runtime. The provider evidence -below proves that the maintained launch configuration accepts the channel. +The server advertises the `experimental.claude/channel` capability. During the +provider's research preview, the maintained direct-server launch uses this +selector: -The maintained launch path must use a provider-approved channel enrollment. -The provider's development-only channel override is observation and test -equipment. It is not production acceptance. +```text +--dangerously-load-development-channels server: +``` -The bounded packaging and policy path is in -[`packaging.md`](./packaging.md). That document does not authorize building the -plugin. +`--channels server:` is not sufficient for a directly declared +server. Claude can connect that server and then reject its notifications because +the server is not on the approved channel allowlist. + +The development selector presents one confirmation when each Claude process +starts. st2 owns the startup PTY. It may confirm only the exact development +channel warning that names the expected server selector. An unrelated prompt, +a different selector, or an untrusted workspace blocks startup and receives no +input. This bounded startup action is not the legacy per-message DING screen +classifier. + +If an organization deploys managed settings, that policy must permit channels +with `channelsEnabled: true`. A deployment with no managed policy does not need +to create a managed settings file: live Claude Code 2.1.226 accepted the direct +development selector with no such file. A managed plugin allowlist can remove +the startup confirmation; [`packaging.md`](./packaging.md) defines that optional +path. + +The MCP wire does not return a channel-acceptance receipt to the server, so the +adapter must not claim that it detects provider acceptance at runtime. The +provider evidence below proves that the maintained launch configuration accepts +the channel. + +The direct-server selector is permitted only for the locally built and pinned +st2 server. It must not approve an arbitrary downloaded server. The bounded +plugin and policy alternative is in [`packaging.md`](./packaging.md). ### Notification @@ -66,9 +89,16 @@ it is `messageFilename`. The adapter does not invent sender, thread, or recipient values from display text. Claude presents an accepted notification as an inbound channel item. When the -session is idle, the item starts a turn without terminal input from st2. This -provider behavior is the native wake receipt. The adapter does not parse the -rendered inbound line and does not wait for a model response. +session is idle, the item starts a turn without terminal input from st2. If a +turn is active, Claude queues the item. More than one waiting item can be +coalesced into the next turn. The adapter must not assume one provider turn for +each notification. + +Claude treats channel content as untrusted input. The model may report or refuse +an instruction in that content. Native DING proves that the item was presented +and woke the session; it does not prove that the model obeyed the content or +produced a particular reply. The adapter does not parse the rendered inbound +line and does not wait for a model response. ### Durable inbox and retry @@ -119,10 +149,30 @@ native adapter is accepted: - The inbound item starts observable work without a user keystroke. - A message sent during active work does not corrupt user input or terminal state. +- Several messages sent during active work remain queued without loss. The + provider may coalesce them into one later turn. - The inbox file remains until the agent reads and archives it. - A deliberately broken notification or disabled transport makes the test fail. +The repository-owned probe in [`tools/channel-probe/`](../../../../tools/channel-probe/) +registers no tools and sends only channel notifications. The 2026-08-11 live +matrix produced these results: + +- Claude Code 2.1.226, with no managed policy file, accepted the direct + development selector. One idle notification started a turn with no user input + after the bounded startup confirmation. +- Claude Code 2.1.227 started turn 1 for notification A. Notifications B and C + arrived 0.1 seconds apart while turn 1 was running. After turn 1 ended, Claude + started turn 2 and presented both waiting items together. All three distinct + identifiers were present. +- The model refused the probe's embedded output instruction because channel + content is untrusted. The refusal does not make delivery fail: the provider + debug events and spontaneous turns prove presentation and wake. +- Claude Code 2.1.226 connected the same class of directly declared server but + rejected channel notifications when the required development selector was + omitted. A connected MCP transport alone is therefore not acceptance. + ## Legacy screen transport The remaining sections define the unchanged screen grammar selected by diff --git a/tools/channel-probe/README.md b/tools/channel-probe/README.md new file mode 100644 index 00000000..7e596e22 --- /dev/null +++ b/tools/channel-probe/README.md @@ -0,0 +1,30 @@ +# Claude channel probe + +This probe registers no MCP tools. It advertises the Claude channel capability +and sends one or more `notifications/claude/channel` notifications. + +Run the idle-wake cell with: + +```sh +./run-probe.sh +``` + +The script creates an isolated temporary workspace. It sends Return only for +the new-workspace trust prompt and the development-channel warning. It reports +success only when the provider debug log records a channel notification and a +later engine turn. The model does not need to obey the notification content. + +Run the active-turn queue cell with three simple identifiers: + +```sh +PROBE_TOKENS=QUEUE-A,QUEUE-B,QUEUE-C PROBE_INTERVAL=100 ./run-probe.sh +``` + +Notification A must start turn 1. Notifications B and C must arrive while turn +1 is active. Both must then produce a completed turn 2. The provider may +coalesce the two waiting notifications into that turn. `PROBE_DELAY` sets the +first delay in milliseconds. The idle default remains one token after 20 +seconds. + +These files are evidence tools. They are not the production `st2 claude-mcp` +adapter. diff --git a/tools/channel-probe/probe-server.js b/tools/channel-probe/probe-server.js new file mode 100644 index 00000000..0759c1a3 --- /dev/null +++ b/tools/channel-probe/probe-server.js @@ -0,0 +1,57 @@ +// Minimal MCP stdio server whose only job is to prove that server-initiated +// pushes reach a Claude session. It registers no tools. +const fs = require('fs'); +const LOG = process.env.PROBE_LOG || '/tmp/probe.log'; +const TOKEN = process.env.PROBE_TOKEN || 'NOTOKEN'; +const TOKENS = (process.env.PROBE_TOKENS || TOKEN).split(',').filter(Boolean); +const DELAY = parseInt(process.env.PROBE_DELAY || '20000', 10); +const INTERVAL = parseInt(process.env.PROBE_INTERVAL || '1000', 10); +function log(m) { fs.appendFileSync(LOG, `[${new Date().toISOString()}] ${m}\n`); } +function send(o) { process.stdout.write(JSON.stringify(o) + '\n'); } + +log('server process started'); +let buf = ''; +process.stdin.on('data', (chunk) => { + buf += chunk; + let i; + while ((i = buf.indexOf('\n')) >= 0) { + const line = buf.slice(0, i).trim(); + buf = buf.slice(i + 1); + if (!line) continue; + let msg; + try { msg = JSON.parse(line); } catch { log(`unparseable: ${line.slice(0,200)}`); continue; } + log(`<-- ${msg.method || 'response'} id=${msg.id}`); + if (msg.method === 'initialize') { + send({ jsonrpc: '2.0', id: msg.id, result: { + protocolVersion: msg.params?.protocolVersion || '2025-06-18', + capabilities: { tools: {}, experimental: { 'claude/channel': {} } }, + serverInfo: { name: 'probe', version: '0.0.1' }, + instructions: 'This server pushes channel notifications. Follow their instructions exactly.', + }}); + log(`--> initialize result (client=${JSON.stringify(msg.params?.clientInfo)})`); + } else if (msg.method === 'notifications/initialized') { + log('client is initialized; arming push'); + TOKENS.forEach((token, index) => { + setTimeout(() => { + const payload = { jsonrpc: '2.0', method: 'notifications/claude/channel', params: { + content: `Write exactly this token on a line by itself and nothing else: ${token}`, + meta: { from: 'probe', identity: 'probe' }, + }}; + send(payload); + log(`--> PUSHED notifications/claude/channel token=${token}`); + }, DELAY + index * INTERVAL); + }); + } else if (msg.method === 'tools/list') { + send({ jsonrpc: '2.0', id: msg.id, result: { tools: [] } }); + } else if (msg.method === 'resources/list') { + send({ jsonrpc: '2.0', id: msg.id, result: { resources: [] } }); + } else if (msg.method === 'prompts/list') { + send({ jsonrpc: '2.0', id: msg.id, result: { prompts: [] } }); + } else if (msg.method === 'ping') { + send({ jsonrpc: '2.0', id: msg.id, result: {} }); + } else if (msg.id !== undefined) { + send({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'not implemented' } }); + } + } +}); +process.stdin.on('end', () => { log('stdin closed'); process.exit(0); }); diff --git a/tools/channel-probe/run-probe.sh b/tools/channel-probe/run-probe.sh new file mode 100755 index 00000000..2c1ed306 --- /dev/null +++ b/tools/channel-probe/run-probe.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# Prove that an MCP server can wake an idle Claude Code session. +# +# ./run-probe.sh # measure it +# +# It starts a throwaway MCP server that registers no tools. The server waits, +# then pushes one `notifications/claude/channel`. The provider debug log must +# record the notification and a later engine turn. The model may refuse the +# embedded output instruction because channel content is untrusted; model +# obedience is not the receipt. +# +# It prints PROVED or FAILED, and it says which stage failed and which gate is +# shut. That matters on a machine where channelsEnabled has never been set: the +# probe tells you whether the development flag alone is enough, which is a +# question we could not answer on a machine that already had the policy set. +set -uo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +WORK="${TMPDIR:-/tmp}/channel-probe-$$" +TOKEN="CHANNEL-PROBE-$RANDOM$RANDOM" +TOKENS="${PROBE_TOKENS:-$TOKEN}" +DELAY="${PROBE_DELAY:-20000}" +INTERVAL="${PROBE_INTERVAL:-1000}" +if [[ ! "$TOKENS" =~ ^[A-Za-z0-9._:-]+(,[A-Za-z0-9._:-]+)*$ ]]; then + echo "PROBE_TOKENS must be a comma-separated list of simple identifiers." >&2 + exit 2 +fi +TOKEN_COUNT=$(awk -F, '{ print NF }' <<<"$TOKENS") + +children_of() { + ps -eo pid=,ppid= | awk -v parent="$1" '$2 == parent { print $1 }' +} + +stop_tree() { + local parent="$1" + local child + [[ "$parent" =~ ^[0-9]+$ ]] || return 0 + [ "$parent" -gt 1 ] || return 0 + for child in $(children_of "$parent"); do + stop_tree "$child" + done + if kill -0 "$parent" 2>/dev/null; then + kill "$parent" 2>/dev/null || true + fi +} + +SPID="" +WPID="" +trap 'if [ -n "$WPID" ]; then stop_tree "$WPID"; fi; if [ -n "$SPID" ]; then stop_tree "$SPID"; fi' EXIT +mkdir -p "$WORK" +cp "$HERE/probe-server.js" "$WORK/" +cat > "$WORK/.mcp.json" </dev/null | grep -qi 'util-linux'; then + script -q -c \ + 'exec claude --debug-file debug.log --dangerously-load-development-channels server:probe --permission-mode bypassPermissions' \ + "$WORK/session.txt" <"$WORK/input.fifo" >/dev/null 2>&1 & +else + script -q "$WORK/session.txt" \ + claude --debug-file "$WORK/debug.log" \ + --dangerously-load-development-channels server:probe \ + --permission-mode bypassPermissions <"$WORK/input.fifo" >/dev/null 2>&1 & +fi +SPID=$! +{ sleep 4; printf '\r'; sleep 4; printf '\r'; sleep 90; } >"$WORK/input.fifo" & +WPID=$! +FOUND="" +for i in $(seq 1 85); do + sleep 1 + if [ -f "$WORK/debug.log" ]; then + FIRST_NOTIFICATION=$(grep -nF 'MCP server "probe": notifications/claude/channel:' \ + "$WORK/debug.log" | head -1 | cut -d: -f1) + FIRST_TURN_START=$(grep -nE '\[engine\] turn [0-9]+ start' "$WORK/debug.log" | + head -1 | cut -d: -f1) + if [ "$TOKEN_COUNT" -eq 1 ]; then + if [ -n "$FIRST_NOTIFICATION" ] && [ -n "$FIRST_TURN_START" ] && + [ "$FIRST_NOTIFICATION" -lt "$FIRST_TURN_START" ]; then + FOUND="$i" + break + fi + else + SECOND_NOTIFICATION=$(grep -nF 'MCP server "probe": notifications/claude/channel:' \ + "$WORK/debug.log" | sed -n '2p' | cut -d: -f1) + LAST_NOTIFICATION=$(grep -nF 'MCP server "probe": notifications/claude/channel:' \ + "$WORK/debug.log" | tail -1 | cut -d: -f1) + FIRST_TURN_END=$(grep -nE '\[engine\] turn [0-9]+ end' "$WORK/debug.log" | + head -1 | cut -d: -f1) + SECOND_TURN_START=$(grep -nE '\[engine\] turn [0-9]+ start' "$WORK/debug.log" | + sed -n '2p' | cut -d: -f1) + SECOND_TURN_END=$(grep -nE '\[engine\] turn [0-9]+ end' "$WORK/debug.log" | + sed -n '2p' | cut -d: -f1) + if [ -f "$WORK/probe.log" ]; then + PUSH_COUNT=$(grep -c -- '--> PUSHED notifications/claude/channel' \ + "$WORK/probe.log" || true) + else + PUSH_COUNT=0 + fi + if [ "$PUSH_COUNT" -eq "$TOKEN_COUNT" ] && + [ -n "$FIRST_NOTIFICATION" ] && [ -n "$FIRST_TURN_START" ] && + [ -n "$SECOND_NOTIFICATION" ] && [ -n "$LAST_NOTIFICATION" ] && + [ -n "$FIRST_TURN_END" ] && [ -n "$SECOND_TURN_START" ] && + [ -n "$SECOND_TURN_END" ] && + [ "$FIRST_NOTIFICATION" -lt "$FIRST_TURN_START" ] && + [ "$FIRST_TURN_START" -lt "$SECOND_NOTIFICATION" ] && + [ "$LAST_NOTIFICATION" -lt "$FIRST_TURN_END" ] && + [ "$FIRST_TURN_END" -lt "$SECOND_TURN_START" ] && + [ "$SECOND_TURN_START" -lt "$SECOND_TURN_END" ]; then + FOUND="$i" + break + fi + fi + fi +done +stop_tree "$WPID" +wait "$WPID" 2>/dev/null || true +WPID="" +stop_tree "$SPID" +wait "$SPID" 2>/dev/null || true +SPID="" +sleep 1 + +echo "workdir: $WORK" +if [ -n "$FOUND" ]; then + if [ "$TOKEN_COUNT" -eq 1 ]; then + echo "PROVED: the notification started an engine turn ${FOUND}s after start." + else + echo "PROVED: ${TOKEN_COUNT} notifications crossed an active turn without loss." + echo "Waiting notifications started a second engine turn ${FOUND}s after start." + fi + echo "No input was sent after the two bounded startup confirmations." + exit 0 +fi +echo "FAILED. The stage that failed:" +CLEAN=$(sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g' "$WORK/session.txt" 2>/dev/null | + tr '\r' '\n') +if printf '%s' "$CLEAN" | grep -qi "org policy"; then + echo " GATE 1 is shut: channels are not enabled by org policy." + echo " Set channelsEnabled: true in managed settings." +elif printf '%s' "$CLEAN" | grep -qi "approved channels allowlist"; then + echo " GATE 3 is shut: this server is not on the approved allowlist." + echo " Add --dangerously-load-development-channels, or allowlist a plugin." +elif [ ! -f "$WORK/probe.log" ]; then + echo " The MCP server never started. Claude did not reach it." + echo " A trust question or a startup error usually causes this." +elif ! grep -q "initialize result" "$WORK/probe.log"; then + echo " The server started, and the handshake did not finish." +elif ! grep -q "PUSHED" "$WORK/probe.log"; then + echo " The handshake finished, and the server never pushed." +else + echo " The server pushed, and the required later engine turn was not observed." + printf '%s' "$CLEAN" | grep -iE "channel" | tail -3 | sed 's/^/ session says: /' +fi +exit 1