Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
eaa574e
RELEASE W5-prep: real-transport get_cookies success path
AminDhouib Jul 25, 2026
c674fe5
RELEASE-5: generated qualified release contract and tool evidence ledger
AminDhouib Jul 25, 2026
bf6b640
RELEASE-5 W5: take the get_cookies transport success path (PR #52)
AminDhouib Jul 25, 2026
5a4bb11
RELEASE-5 W5: qualify the cookie round trip, and say what that costs
AminDhouib Jul 25, 2026
6fcaaa0
RELEASE 2.0.0: version bump and changelog
AminDhouib Jul 25, 2026
57609e3
RELEASE-5 W5: claim the cookie tools, and write the contract as a SHI…
AminDhouib Jul 25, 2026
05e1a6b
RELEASE-5 W5: a drift failure prints the diff, not just the word "stale"
AminDhouib Jul 25, 2026
bece37f
RELEASE 2.0.0: record the per-tool verification depth honestly
AminDhouib Jul 25, 2026
be115b8
RELEASE 2.0.0: stop shipping 14 MB of demo media and audit docs in th…
AminDhouib Jul 25, 2026
5bc09e9
RELEASE-5 W5: fix the PRODUCER of the node ids, and stop "unqualified…
AminDhouib Jul 25, 2026
5028d66
RELEASE-5 W5: record the Linux cold-spawn flake where it actually landed
AminDhouib Jul 25, 2026
5c2505a
RELEASE-5: merge the 2.0.0 version bump into the W5 contract line
AminDhouib Jul 25, 2026
30700bb
RELEASE-5: generate the contract at 2.0.0, the version it actually de…
AminDhouib Jul 25, 2026
d13997e
RELEASE-5: regenerate the contract at 2.0.0
AminDhouib Jul 25, 2026
6a8fa79
RELEASE-5: re-run the gate to arbitrate the macOS teardown flake
AminDhouib Jul 29, 2026
2e7cd33
RELEASE-5: record the macOS gate flake the 2.0.0 re-run exposed (F-779)
AminDhouib Jul 29, 2026
a97c970
RELEASE 2.0.0: sync uv.lock to the version pyproject already declares
AminDhouib Jul 29, 2026
7c65374
RELEASE-5: explain the three red PRs that look like a FIX-B regressio…
AminDhouib Jul 29, 2026
7a3f546
RELEASE-5: F-779 reproduced on a docs-only commit — correct the rate …
AminDhouib Jul 29, 2026
3be448b
RELEASE-5: name a probable mechanism for F-779 instead of shrugging a…
AminDhouib Jul 29, 2026
2f6e813
RELEASE-5: stop chasing the F-779 percentage; state the durable claim
AminDhouib Jul 29, 2026
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
375 changes: 372 additions & 3 deletions .github/workflows/release-gate.yml

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ node_modules/
htmlcov/
.env

# plan_RELEASE W5: the release-evidence/v1 ledger is CI OUTPUT for one run
# (release-evidence/<release_sha>/...). It is uploaded as a run artifact and
# re-read by the `release-evidence` job; a copy committed to the tree would be
# stale evidence for a SHA that is no longer HEAD — exactly what the aggregate's
# stale-SHA check exists to reject.
release-evidence/

# Clone / large-response artifacts. The default output dir is now
# ~/.stealth-mcp/element_clones (outside the repo), but never commit any that
# land in-tree via an explicit relative path or a stale pre-fix run.
Expand Down
132 changes: 132 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Changelog

## 2.0.0

The first release since the foundational audit. It carries ~85 commits since 1.2.0 and
fixes four defects that were present in every prior release and invisible to the old
test suite, because none of them can be reproduced through the in-process test seam —
they only appear over the real stdio transport a client actually uses.

### ⚠️ Breaking

- **`STEALTH_MCP_SESSION_STORAGE_CAP_GB` is now `STEALTH_MCP_BROWSER_SESSION_STORAGE_CAP_GB`.**
- **`--session-cap-gb` is now `--browser-session-cap-gb`.**

There is **no back-compat alias**. The old names are simply not read, so if you set
either one it stops taking effect **silently** on upgrade — your storage cap reverts to
the default. The rename removes a genuine ambiguity: "session" meant three different
things across this codebase (an MCP protocol session, a Claude Code session, and a
profile-backed browser session), and only the last one was ever meant here.

Note the environment namespace is strict: an unrecognised `STEALTH_MCP_*` variable is
rejected at startup rather than ignored, so a stale name fails loudly at the *next*
restart even though the setting itself silently lapsed.

### Fixed

- **Browsers were destroyed every ~2 seconds over real stdio.** FastMCP runs the server
lifespan once per *MCP session*, and the liveness watchdog's probe sessions each re-ran
orphan recovery and its destructive teardown — killing every live browser instance
belonging to the real session. Anyone driving this server the normal way (stdio proxy →
detached backend) had instances disappear underneath them. The lifespan is now
session-reentrant.
- **`list_tabs` raised a bare `TypeError` after any `close_tab`.** nodriver re-adds
rediscovered targets as raw `Connection` objects, which are not awaitable, and the tool
awaited each one. Once a tab had been closed the failure was permanent for that
browser, not transient.
- **Every navigation after a `close_tab` silently switched tabs and leaked one.** The
same root cause, but swallowed by a broad exception handler: the tracked tab was found
correctly, the liveness check on it raised, and the handler concluded the tab was
"missing or invalid" and replaced it — without closing the original. No error surfaced;
navigation simply happened in a different tab each time, and the abandoned tabs
accumulated.
- **`close_tab` returned `False` for a closeable tab**, and **`switch_to_tab` failed to
activate**, for the same class of rediscovered target. Both now address the target by
id through CDP, which works regardless of object type.
- **Headless mode advertised `HeadlessChrome` in its User-Agent.** That is the cheapest
bot check that exists — one server-side substring test, before any JavaScript runs —
and it contradicted the product's central claim. A default headless spawn now presents
the same User-Agent the same binary presents headed, on the page, on the wire, and at
the CDP level. An explicitly supplied `user_agent` still wins. See *Known limitations*
for what this does **not** fix.
- **Every spawn enabled catch-all network interception, even with no hooks defined.**
Chrome paused every request and waited for a resume that only the hook handler would
send, so all traffic paid a pause plus a CDP round-trip for no benefit. Interception is
now armed only when there is something to intercept — and, relatedly, a hook created
*after* spawn now arms interception through the same path instead of relying on the
catch-all's accidental coverage.
- **Selector resolution could hit stale-node `-32000` errors under DOM churn**, because
nodriver's `select`/`find`/`query_selector` are not atomic. All selector resolution now
routes through a single resolver that survives document-node invalidation.
- A Tier-A pass on silent-correctness and "lying success" defects — cases where a tool
reported success without having done the thing (PR #41).

### Added

- A **three-OS release gate** (Ubuntu x64, Windows x64, macOS ARM64) that exercises the
real stdio transport against real Chrome, asserts the exact Chrome binary identity, and
gates on a single aggregate check. Previous releases were verified on Ubuntu only.
- **Build-once packaging.** The distribution is built exactly once per commit, hashed,
verified, and installed from that same artifact in smoke tests; the publish step
downloads those bytes, re-checks their SHA-256, and uploads them without rebuilding.
What was tested is what ships.
- A **deterministic offline stealth suite** asserting anti-detection invariants against a
vanilla-Chrome control, so a regression that reintroduces an automation tell fails the
build.
- The **source distribution shrank from 15 MB to 592 KB.** It had been shipping 12.8 MB
of demo media and 2.8 MB of internal audit documents. The wheel — what `pip` and `uvx`
actually install — is unchanged at 192 KB; this only affects installing from source.

### Known limitations

Stated explicitly rather than by omission.

- **macOS: navigation is unverified.** On GitHub-hosted macOS/ARM64 runners, Chrome
launched by the detached backend completes no network navigation (reproducible 11/11);
a connection to a *closed* port hangs rather than being refused, so the request never
reaches the network stack. The cause is unknown and it has **never been reproduced on a
real Mac** — hosted runners differ in ways that plausibly matter. The gate therefore
excludes the macOS transport cell and runs macOS install-smoke without navigation. This
release makes **no claim that macOS navigation works, and none that it is broken.**
Linux x64 and Windows x64 are verified.
- **Headless is not "undetectable".** The User-Agent fix closes the cheapest and most
widely deployed check, but supplying a User-Agent override makes Chrome blank its
high-entropy client hints (`architecture`, `bitness`, `platformVersion`,
`uaFullVersion`, `fullVersionList`). Low-entropy hints and every `sec-ch-ua*` header on
the wire remain correct and coherent, so the residue is reachable only from JavaScript
that explicitly calls `getHighEntropyValues()` — a strictly smaller and more expensive
tell than the one it replaces, but a real one.
- **`switch_to_tab` can still store a rediscovered target** as an instance's main tab.
Activation is fixed; the storage path is not. It fails loudly if it fires.
- **The HTTP transport is unauthenticated and loopback-default by design.** All
verification here covers stdio; stdio evidence licenses no HTTP claim.
- **Not evidenced in this release:** scheduled drift observation, deterministic
site-breadth corpus, manual-QA parity tripwire, performance and resource budgets,
fault-injection/resilience, runnable-documentation checks, the security/trust-boundary
matrix, wire concurrency/cancellation and independent-client interoperability,
upgrade/migration smoke, failure-observability, and worker/PWA/internationalized site
shapes. These are planned work that has **not** been performed — do not read their
absence as a passing result.
- **Per-tool verification depth varies, and the release contract says so per tool.** All
94 tools have end-to-end coverage driving real Chrome against a local fixture,
enforced by a set-equality tripwire (94 covered, 0 exempt). But only `set_cookie`,
`get_cookies` and `clear_cookies` are additionally verified over the **real stdio
transport** your client actually speaks; the rest are exercised through an in-process
test seam that bypasses the wire. That distinction is not academic — every one of the
transport bugs fixed above was invisible to the seam. It is a gap in test placement
rather than a known defect in those 91 tools, which is why the contract calls them
*served* rather than *release-qualified*.
- A known flake exists in one packaging smoke cell (`install-smoke (sdist Linux/X64)`) on
Chrome cold-spawn; it passes on re-run.

### Upgrading from 1.x

1. Rename `STEALTH_MCP_SESSION_STORAGE_CAP_GB` → `STEALTH_MCP_BROWSER_SESSION_STORAGE_CAP_GB`
and `--session-cap-gb` → `--browser-session-cap-gb` wherever you set them.
2. Pin the new version, e.g. `uvx stealth-chrome-devtools-mcp==2.0.0`.
3. Restart the backend. Code changes apply via a fresh backend process — the singleton is
version-gated, so an old backend is evicted rather than reused.

## 1.2.0 and earlier

Not tracked in this file; see the repository history.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Add to your MCP config (`claude_desktop_config.json`, `.claude/settings.json`, e
"mcpServers": {
"stealth-chrome-devtools-mcp": {
"command": "uvx",
"args": ["stealth-chrome-devtools-mcp==1.2.0"]
"args": ["stealth-chrome-devtools-mcp==2.0.0"]
}
}
}
Expand All @@ -58,7 +58,7 @@ Add to your MCP config (`claude_desktop_config.json`, `.claude/settings.json`, e
Or install via pip:

```bash
pip install stealth-chrome-devtools-mcp==1.2.0
pip install stealth-chrome-devtools-mcp==2.0.0
```

### Local Development
Expand Down
Loading
Loading