diff --git a/AGENTS.md b/AGENTS.md index ecd1b4a..6229af6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Command sources: `.github/workflows/ci.yml` (the gates), `.github/workflows/rele - **Type:** app — published as a PyPI wheel (`uvx blc-ssh-mcp`) *and* a container image (`ghcr.io/blackaxgit/ssh-mcp`). **The PyPI distribution is `blc-ssh-mcp`, not `ssh-mcp`** — that name belongs to an unrelated project this repo has never owned; see the comment above `name` in `pyproject.toml`. The import package is still `ssh_mcp` and the image is still `.../ssh-mcp`. - **Stacks:** Python 3.11-3.14, `uv` + hatchling, asyncssh, MCP SDK, Starlette/uvicorn (HTTP transport only) - **Deploy targets:** N/A — this is a tool operators run themselves, not a hosted service -- **Version:** `src/ssh_mcp/__init__.py` is the single source (hatchling reads it). Currently `0.8.1`. `0.7.0` shipped 2026-09-06 (the MCP SDK v2 migration) and is the first release published under a reviewer-gated `pypi` environment; `0.6.0`, `0.6.1` and `0.6.2` all shipped 2026-07-26. `CHANGELOG.md` `[Unreleased]` is where post-0.8.0 work accrues. +- **Version:** `src/ssh_mcp/__init__.py` is the single source (hatchling reads it). Currently `0.9.0`. `0.7.0` shipped 2026-09-06 (the MCP SDK v2 migration) and is the first release published under a reviewer-gated `pypi` environment; `0.6.0`, `0.6.1` and `0.6.2` all shipped 2026-07-26. `CHANGELOG.md` `[Unreleased]` is where post-0.9.0 work accrues. **What this tool does matters for how you treat it.** Every tool call runs a shell command or transfers a file on a remote host. A bug here is not a crashed request; it is an unintended command on someone's infrastructure, or a write to the operator's own machine. @@ -20,7 +20,7 @@ Command sources: `.github/workflows/ci.yml` (the gates), `.github/workflows/rele ```bash uv sync --locked --extra dev # setup. --locked is required; see Gotchas -uv run pytest # full suite (853 tests, seconds) +uv run pytest # full suite (855 tests, seconds) uv run pytest tests/test_ssh.py::TestRedactSecrets -v # one class uv run pytest 'tests/test_ssh.py::TestRedactSecrets::' -v # one test uv run pytest -k "confinement" -v # by keyword diff --git a/CHANGELOG.md b/CHANGELOG.md index 2705d80..1296f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -> **The next release is 0.9.0, not 0.8.2:** the `groups` fix under Fixed makes two configurations that started on 0.8.1 refuse to start, and by the rule recorded under 0.8.1 below that takes the minor. +## [0.9.0] - 2026-09-30 + +> **Why 0.9.0 and not 0.8.2:** the `groups` fix under Fixed makes two configurations that started on 0.8.1 refuse to start, a string `groups` and an inline-table one, which is the test 0.8.0 recorded for taking the minor. The fix for both is a list: `groups = ["web"]`. ### Security **PyJWT is held at 2.14.0 or later, clearing ten advisories that turned the `pip-audit` gate red on `main`.** 2.13.0, locked transitively through `mcp`, carries CVE-2026-101917, CVE-2026-102265 to CVE-2026-102269 and CVE-2026-102271 to CVE-2026-102274: four algorithm-confusion variants, an empty-HMAC-key bypass, lenient Base64URL signature decoding, a followed JWKS redirect, and three denial-of-service paths in JWKS fetching and token/JWKS parsing. None is reachable here: the only `import jwt` in mcp 2.2.0 is `mcp/client/auth/extensions/client_credentials.py`, an OAuth *client* extension ssh-mcp never imports; its HTTP transport authenticates with its own bearer-token middleware. +**urllib3 is locked at 2.8.0, clearing CVE-2026-97687, CVE-2026-97688 and CVE-2026-97689, which turned the `pip-audit` gate red on `main` after #68 merged.** urllib3 is a development-only dependency here, pulled in through `pip-audit` -> `requests`. The wheel does not depend on it, and the container image installs with `--no-dev`, so neither shipped artifact carried the vulnerable version. The floor is still declared, as a `[tool.uv]` constraint, because this project's rule is that every security floor lives in `pyproject.toml` whether or not a runtime path reaches it; `pip`'s dev-only floor is the precedent. + ### Changed **Stateful HTTP sessions now expire after 30 idle minutes, and at most 10,000 are held at once.** Both limits are mcp 2.2.0 defaults (#64), which the lockfile and container image now ship, and they apply only to stateful sessions on the legacy (2025-11-25 and earlier) protocol. A session with no request in flight and no open GET stream is closed after 1800 s; its next request gets `404` `Session not found`, which a client that does not re-initialize surfaces as an error. An open GET stream prevents expiry but still counts toward the cap, and a session opened beyond the cap gets `503`. Stateless mode (`SSH_MCP_HTTP_STATELESS=true`) avoids both limits. ssh-mcp deliberately exposes no knob for either: both bound server-side memory on an endpoint that executes shell commands. The declared floor stays `mcp>=2.1.1`, so an existing environment that already has 2.1.1 keeps the old unbounded behaviour until it upgrades. diff --git a/pyproject.toml b/pyproject.toml index 390bd23..e450b9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -179,6 +179,12 @@ constraint-dependencies = [ # keeps the lockfile/container image clear and a future `uv lock` from # regressing. "pyjwt>=2.14.0", + # CVE-2026-97687, CVE-2026-97688, CVE-2026-97689 — three urllib3 + # advisories fixed in 2.8.0. Transitive and dev-only: pip-audit -> requests + # -> urllib3, so neither the wheel nor the `--no-dev` container image + # carries it; the floor keeps a future `uv lock` from regressing the dev + # environment that CI's audit job runs in. + "urllib3>=2.8.0", ] [tool.ruff.lint] diff --git a/src/ssh_mcp/__init__.py b/src/ssh_mcp/__init__.py index ab68aae..0a57f26 100644 --- a/src/ssh_mcp/__init__.py +++ b/src/ssh_mcp/__init__.py @@ -1,3 +1,3 @@ """SSH MCP Server - Manage infrastructure via MCP clients like Claude Code.""" -__version__ = "0.8.1" +__version__ = "0.9.0" diff --git a/tests/test_dependency_floors.py b/tests/test_dependency_floors.py index 9a58501..3be2491 100644 --- a/tests/test_dependency_floors.py +++ b/tests/test_dependency_floors.py @@ -99,6 +99,12 @@ "mcp.client.auth.extensions.client_credentials, the only importer, " "so none is reachable here.", ), + "urllib3": ( + "2.8.0", + "CVE-2026-97687, CVE-2026-97688 and CVE-2026-97689, all fixed in 2.8.0. " + "Transitive and dev/CI-only (pip-audit -> requests -> urllib3); absent " + "from the wheel's dependencies and from the --no-dev container image.", + ), } # Packages that ssh-mcp imports directly and must therefore constrain in the diff --git a/uv.lock b/uv.lock index c8b75a1..a61f081 100644 --- a/uv.lock +++ b/uv.lock @@ -13,6 +13,7 @@ constraints = [ { name = "click", specifier = ">=8.3.3" }, { name = "cryptography", specifier = ">=50.0.1" }, { name = "pyjwt", specifier = ">=2.14.0" }, + { name = "urllib3", specifier = ">=2.8.0" }, ] [[package]] @@ -2056,11 +2057,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [[package]]