Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ operator, corporate group, ownership, or control.

### Fixed

- `recon mcp doctor` no longer reports a cleanup error in place of the
handshake failure that caused it. The server is spawned with an empty
tempdir as its cwd, and on Windows that directory stays locked until the
child has fully exited, so tearing the transport down after a failed
handshake could raise `PermissionError: [WinError 32]` out of the tempdir's
own cleanup and overwrite the diagnosis with it. Cleanup of that isolation
boundary is now best-effort; the cwd-shadow defense it exists for is
unchanged. Seen as an intermittent Windows CI failure in
`tests/test_mcp_doctor.py`, with a platform-independent regression test.
- `docs/statistical-assurance.md` rendered its signed marginal entropy formula
as literal bracketed LaTeX rather than math, so github.com printed the raw
expression between plain brackets. Same defect class as the
Expand Down
9 changes: 6 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ rows. Detail: [docs/mcp-2026-07-28-readiness.md](docs/mcp-2026-07-28-readiness.m

### 3. Product-quality baseline - **next build priority: v2.11**

Specified, decision rule frozen, measurement not started. Process evidence is
rich; product-outcome evidence is not. Acceptance and stop rules live in
Specified, decision rule frozen, measurement part-run: the network-free half of
the Phase 1 baseline is frozen, and the ablation that decides fusion's fate has
not started. Process evidence is rich; product-outcome evidence is not.
Acceptance and stop rules live in
[docs/roadmap.md](docs/roadmap.md#3-establish-a-reproducible-product-quality-baseline)
and the
[Quality Proof execution plan](docs/strategic-gap-audit.md#quality-proof-execution-plan).
Expand All @@ -87,7 +89,8 @@ reference-positive and 183 reference-negative labeled units.

`scripts/quality_scorecard.py` emits the public half of the Phase 1 baseline,
network-free and corpus-free, and names every channel it cannot measure without
the private corpus or live providers.
the private corpus or live providers. Its first dated run is
[validation/2026-08-05-quality-baseline-scorecard.md](validation/2026-08-05-quality-baseline-scorecard.md).

### 4. Optional operator-hosted access - lower priority side track

Expand Down
9 changes: 6 additions & 3 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ drift reopens priority 1. Version milestones through v3.0:
|---|---|---|---|
| [1. Evidence-semantic integrity](#1-restore-evidence-semantic-integrity) | Truthfulness outranks features, and this defect class required a complete sweep rather than one-case fixes. | Complete on 2026-08-01. The fail-closed default-claim audit owns all discovered primary surfaces through 27 families. 27 are complete; 0 material runtime families carry incomplete lineage. Static agent and MCP contracts now pin process scope, collection boundaries, output forms, cache behavior, and abstention semantics. Runtime explanations, insights, panels, service labels, posture observations, hardening prompts, and every exposure-index component carry their reviewed evidence or static contract basis. | Keep the fail-closed audit green; any uncovered or semantically stronger surface reopens this track. |
| [2. MCP protocol characterization](#2-keep-final-mcp-v2-compatibility-green-after-adoption) | The 2026-07-28 specification is a breaking release and the SDK moves regardless of recon, so compatibility must stay explicit. | Production adopted `mcp>=2.0.0,<3` on 2026-07-31. The exact stable `1.28.1` rollback and `2.0.0` production rows remain blocking. | Keep deterministic ordering, conforming schemas, live stdio behavior, and both exact stable pins green. |
| [3. Product-quality baseline](#3-establish-a-reproducible-product-quality-baseline) | Depends on a stable claim taxonomy from priority 1. Measuring still-incomplete claim families would measure a definition that is changing. | Specified, not started. Extensive process evidence exists; product-outcome evidence does not. | A dated aggregate-safe scorecard with a decision rule written before the run, deciding whether advanced fusion stays primary or becomes an advanced diagnostic. |
| [3. Product-quality baseline](#3-establish-a-reproducible-product-quality-baseline) | Depends on a stable claim taxonomy from priority 1. Measuring still-incomplete claim families would measure a definition that is changing. | Specified, decision rule frozen, Phase 1 part-run. Extensive process evidence exists; product-outcome evidence does not. | A dated aggregate-safe scorecard with a decision rule written before the run, deciding whether advanced fusion stays primary or becomes an advanced diagnostic. |
| [4. Optional cloud access and scale-out](#4-optional-operator-hosted-access-and-scale-out) | Useful accessibility and scale polish for some operators, but lower priority than the three core evidence and compatibility tracks. | Draft stateless remote adapter, container, and Cloud Run Terraform pass local artifact checks but are not yet provider-validated. Local remains the default. | One operator proof plus bounded load, cost, rotation, retention, and rollback evidence. Each additional provider needs named demand and its own validation context. |

Everything blocked behind these, and the gate that unblocks each, is in
Expand Down Expand Up @@ -280,8 +280,11 @@ Detailed work and rollback criteria live in

### 3. Establish a reproducible product-quality baseline

Status: specified, decision rule frozen, measurement not started. Depends on the
claim taxonomy from priority 1.
Status: specified, decision rule frozen, measurement part-run. The network-free,
corpus-free half of the Phase 1 baseline is frozen in
[2026-08-05-quality-baseline-scorecard.md](../validation/2026-08-05-quality-baseline-scorecard.md);
the corpus- and network-bound channels and the ablation itself have not started.
Depends on the claim taxonomy from priority 1.

The phase order, promotion evidence, and stop rules are summarized in the
[Quality Proof execution plan](strategic-gap-audit.md#quality-proof-execution-plan).
Expand Down
14 changes: 13 additions & 1 deletion src/recon_tool/mcp_client/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,19 @@ async def _run_handshake(
# * an empty tempdir guarantees no Python files exist there,
# * it isolates the child filesystem footprint from the doctor's,
# * cleanup is automatic via the context manager.
with tempfile.TemporaryDirectory(prefix="recon-mcp-doctor-cwd-") as safe_cwd:
#
# ``ignore_cleanup_errors`` keeps that cleanup from rewriting the
# diagnosis. On Windows the directory stays locked until the server
# process it was the cwd of has fully exited, so a handshake failure
# that tears the transport down can lose the race and surface as
# ``PermissionError: [WinError 32]`` - the tempdir's exit exception
# replacing the handshake error the doctor exists to report. The
# directory is an isolation boundary, not state the report depends
# on; a leftover empty tempdir is strictly better than a masked cause.
with tempfile.TemporaryDirectory(
prefix="recon-mcp-doctor-cwd-",
ignore_cleanup_errors=True,
) as safe_cwd:
params = StdioServerParameters(
command=sys.executable,
args=["-m", "recon_tool.server"],
Expand Down
60 changes: 60 additions & 0 deletions tests/test_mcp_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import asyncio
import io
import json
from typing import Any

import pytest
from typer.testing import CliRunner
Expand Down Expand Up @@ -338,6 +339,65 @@ async def _fail_entry(_session: ClientSession) -> ClientSession:
assert report.checks[-1].status == "fail"
assert "synthetic client session entry failure" in report.checks[-1].detail

def test_locked_isolation_cwd_does_not_mask_the_handshake_error(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A cwd that will not delete must not rewrite the diagnosis.

The handshake spawns the server with an empty tempdir as its cwd.
Windows keeps that directory locked until the child has fully
exited, so tearing the transport down after a failed handshake can
lose the race and raise ``PermissionError: [WinError 32]`` out of
the tempdir's cleanup - replacing the very error the doctor exists
to report.

The lock stands in at the context-manager boundary rather than at
``os.rmdir``. Refusing the syscall would exercise ``tempfile``'s
internal retry ladder, which differs by version and platform and
recurses without bound on 3.11, instead of exercising the doctor.
"""
import tempfile

from mcp import ClientSession

real_tempdir = tempfile.TemporaryDirectory
locked: list[bool] = []

class _LockedTemporaryDirectory:
"""Delete normally, then report the directory as still held."""

def __init__(self, **kwargs: Any) -> None:
self._ignore_errors = bool(kwargs.pop("ignore_cleanup_errors", False))
self._is_isolation_cwd = str(kwargs.get("prefix", "")).startswith("recon-mcp-doctor-cwd-")
self._inner = real_tempdir(**kwargs)

def __enter__(self) -> str:
return self._inner.__enter__()

def __exit__(self, *_exc: object) -> None:
self._inner.cleanup()
if self._is_isolation_cwd:
locked.append(self._ignore_errors)
if not self._ignore_errors:
raise PermissionError(
32,
"The process cannot access the file because it is being used by another process",
)

async def _fail_entry(_session: ClientSession) -> ClientSession:
raise RuntimeError("synthetic client session entry failure")

monkeypatch.setattr(ClientSession, "__aenter__", _fail_entry)
monkeypatch.setattr(tempfile, "TemporaryDirectory", _LockedTemporaryDirectory)

report = run_doctor()

assert locked == [True], "the handshake did not ask for best-effort cwd cleanup"
assert [check.name for check in report.checks] == ["server spawn", "client session"]
assert report.checks[-1].status == "fail"
assert "synthetic client session entry failure" in report.checks[-1].detail

def test_real_client_session_cleanup_failure_preserves_protocol_checks(
self,
monkeypatch: pytest.MonkeyPatch,
Expand Down
Loading