Verified integration of the overnight PRs #96/#97/#98/#99 into one landing PR. Supersede + close those four on merge.
Why one PR instead of merging the four
#98 (#92, version) was branched as a sibling of #97 (#91, sidecar) — both off #90 — rather than stacked on it. So merging them in sequence conflicts in bcc_core.py + tests/test_core.py (each just adds an independent section at the same anchor; each PR is green alone, so CI didn't surface it). Resolved as a union (keep both sections/tests) + a two-blank-line ruff format touch-up. #93 then merges clean. The clean chain was 90→91→93; #92 was the odd one.
Verified on the integrated tree (all four together)
#90 behaviour-identity holds: FLAG_ENV_MIGRATIONS is now a derived view of SERVER_SPECS; all #89 migration tests pass unchanged, and --sudoPassword/--suPassword now auto-migrate to SSH_MCP_SUDO_PASSWORD as intended (one #89 test updated intentionally for that data growth).
Cardinal rule intact: apply_servers still only writes mcpServers/_disabledMcpServers. No network. Only bcc_core.py / bcc.py / tests/test_core.py touched.
Reviewer caveats (from the overnight report, confirmed)
GUI click-throughs still pending — the new advisory rows are only headless-smoke-tested. Verify live (ideally with a real ssh-mcp config.toml): #91 args-inert + wrong-path lines; #92 version badge + "Pin to <v>"; #93 permission warning + "Fix permissions"; and that all new rows hide for remote servers / no selection.
#91count_toml_profiles is a heuristic (counts TOML headers; ssh-mcp's real multi-profile schema wasn't verifiable). Advisory-only — never edits/blocks. Don't let it drive anything stronger until confirmed.
#93 "Fix permissions" chmods the sidecar — the only new action that writes to the filesystem (on explicit click); everything else is read-only.
Do not merge until the GUI click-throughs pass.
Closes #90. Closes #91. Closes #92. Closes #93.
Verified integration of the overnight PRs **#96/#97/#98/#99** into one landing PR. Supersede + close those four on merge.
## Why one PR instead of merging the four
#98 (#92, version) was branched as a **sibling** of #97 (#91, sidecar) — both off #90 — rather than stacked on it. So merging them in sequence conflicts in `bcc_core.py` + `tests/test_core.py` (each just adds an independent section at the same anchor; each PR is green alone, so CI didn't surface it). Resolved as a **union** (keep both sections/tests) + a two-blank-line `ruff format` touch-up. #93 then merges clean. The clean chain was 90→91→93; #92 was the odd one.
## Verified on the integrated tree (all four together)
- `python -m pytest` — **green**, 1 skipped (509→529 passing).
- `ruff check .` + `ruff format --check .` — clean.
- `python -m py_compile bcc.py` — OK.
- **#90 behaviour-identity holds**: `FLAG_ENV_MIGRATIONS` is now a derived view of `SERVER_SPECS`; all #89 migration tests pass unchanged, and `--sudoPassword`/`--suPassword` now auto-migrate to `SSH_MCP_SUDO_PASSWORD` as intended (one #89 test updated intentionally for that data growth).
- Cardinal rule intact: `apply_servers` still only writes `mcpServers`/`_disabledMcpServers`. No network. Only `bcc_core.py` / `bcc.py` / `tests/test_core.py` touched.
## Contents
- **#90** ServerSpec spine (pure core): `ServerSpec` dataclass + `SERVER_SPECS` registry (env/removed/drift flags, sidecar paths, schema); `FLAG_ENV_MIGRATIONS` derived from it; sudo/su + `--maxChars=none` drift added.
- **#91** Sidecar detection + precedence: verified per-platform TOML path, args-inert / wrong-path / #11 credential-scoping advisories; read-only. No sidecar writer (cardinal rule).
- **#92** Version pin/drift: detect unpinned npx specs, resolve version from local npx cache (no network), one-click pin, drift note.
- **#93** Permission pre-flight: `mode & 0o077` check (file 0600 / dir 0700, POSIX-only) + "Fix permissions" button.
## Reviewer caveats (from the overnight report, confirmed)
1. **GUI click-throughs still pending** — the new advisory rows are only headless-smoke-tested. Verify live (ideally with a real ssh-mcp `config.toml`): #91 args-inert + wrong-path lines; #92 version badge + "Pin to `<v>`"; #93 permission warning + "Fix permissions"; and that all new rows hide for remote servers / no selection.
2. **#91 `count_toml_profiles` is a heuristic** (counts TOML headers; ssh-mcp's real multi-profile schema wasn't verifiable). Advisory-only — never edits/blocks. Don't let it drive anything stronger until confirmed.
3. **#93 "Fix permissions" `chmod`s the sidecar** — the only new action that writes to the filesystem (on explicit click); everything else is read-only.
Do not merge until the GUI click-throughs pass.
Introduce the server-package axis (orthogonal to ClientSpec): a frozen
ServerSpec dataclass + SERVER_SPECS registry that the sidecar (#91),
version-pin (#92) and permission (#93) work all hang off.
- ServerSpec carries env_flags / removed_flags / drift_flags / sidecar_paths
/ sidecar_doc_path / schema. FLAG_ENV_MIGRATIONS is now a derived view of
the registry, so every existing reader and the migration functions keep the
exact shape #89 shipped — the migration LOGIC is unchanged, only the DATA grew.
- resolve_server_spec(data) is the ServerSpec entry point; detect_migratable_package
is a thin name-only wrapper over it (behaviour identical).
- Add the verified #4 follow-ups: --sudoPassword AND --suPassword now auto-migrate
to SSH_MCP_SUDO_PASSWORD (two flags → one var; existing no-clobber handles it).
--disableSudo stays warn-only (sudo is now a role/policy, no env replacement).
- Add drift_warnings(): --maxChars=none changed meaning (v1 silently capped at
5000 chars). Warn-only, no auto-fix; matches inline and separate arg forms.
- Seed ssh-mcp's verified per-platform sidecar TOML paths and zod enums
(auth/approvalMode/role/port) as data for later issues.
Pure core + tests, no GUI. One #89 test (sudoPassword now migratable) updated to
reflect the intended data growth, with a comment citing the verified facts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ssh-mcp v2 reads a TOML sidecar and only falls back to CLI args when that file
is ABSENT — so BCC's managed --host/--user args can be completely inert while
the real config lives in a file BCC never looks at. This adds read-only
truth-telling for that (no sidecar writing).
Core (pure, fully injectable platform/environ/home/exists/read for testing):
- sidecar_path(spec): VERIFIED per-platform TOML location from the package source,
NOT the README (macOS → ~/Library/Application Support/ssh-mcp, Windows → %APPDATA%,
else → ${XDG_CONFIG_HOME:-~/.config}). sidecar_doc_path() is the README path.
- sidecar_status(): resolves exists / has_managed_args / args_inert / wrong_path.
- sidecar_warnings(): mirrors removed_flag_warnings' shape. Reports:
* precedence — "these arguments are inert; the server reads <real path>"
* wrong-path — a TOML at the README path the server never actually reads
* #11 credential scoping — an unprefixed SSH_MCP_PASSWORD shared across 2+
profiles in a multi-profile sidecar (count_toml_profiles is a documented
3.10-safe heuristic; unscoped_credential_warning gates on it).
GUI: a read-only advisory label in the stdio editor (mirrors the removed-flag
label; no fix button — editing the sidecar is a separate deliberate action).
Uses the real platform/env/filesystem so it reflects this machine.
pytest green, ruff + format clean. Closes#91. Part of epic #94.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`npx -y ssh-mcp` resolves *latest* on every launch — in one session ssh-mcp went
v1 → v2 and the tool set changed under a running agent, mid-task, with no warning.
This detects that and turns it into a comprehensible pin/upgrade prompt.
Core (pure, no network — reuses parse_version / is_newer_version / the catalog
spec parsers; the resolved-version lookup is fully injectable for tests):
- server_package_spec / server_package_name — the npm spec an npx-style server runs.
- is_unpinned_spec — bare name or dist-tag (@latest/@next) is unpinned; an exact
numeric version is pinned.
- resolved_npx_version — reads the local ~/.npm/_npx cache (highest version wins),
degrades to None cleanly. NO network. find/read injectable.
- pin_spec_transform — rewrite the spec to name@version (mirrors pin_command_path's
(new_data, note) contract). No-op when already pinned / bad version / not npx.
- version_drift_note — "moved X → Y since you pinned" via numeric is_newer_version.
- version_status — the badge's high-level dict (unpinned / pinned_version /
resolved_version / can_pin / drift). A _UNSET sentinel lets callers force an
explicit resolved=None ("unknown") vs. omitting it to do the local lookup.
GUI: a version badge row under the dependency status (mirrors that surface) with a
one-click "Pin to <version>" button, shown only for npx servers; a drift note when
a pinned version has been overtaken. Smoke-tested headlessly.
pytest green (520 passed), ruff + format clean. Closes#92. Part of epic #94.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CI matrix includes a windows-latest job where str(Path("/Users/…")) renders
with backslashes, so the platform-parameterised sidecar-path assertions failed
there (macOS/Linux passed). Compare .as_posix() instead — separator-normalised
and portable — and note why. Pure test fix; no production-code change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ssh-mcp refuses to start if its config is group/world-readable (mode & 0o077 →
throws, requiring dir 0700 / file 0600). A GUI user has no idea what chmod 600
means — they just get a dead server. This checks it and offers a one-click fix.
Core (pure, POSIX-only, injectable stat/chmod so tests never touch a real file):
- permission_status(path): the ssh-mcp rule — any group/other bit set (mode & 0o077)
is not-ok; file must be 0600, its dir 0700. Returns None on Windows (modes don't
apply) or when the file is absent (nothing to pre-flight). Plain-language problems
naming the offending octal mode.
- fix_permissions(path): chmod file → 0600, dir → 0700. No-op on Windows; reports
an OSError instead of raising.
- sidecar_permission_warnings() / sidecar_permission_fix_target(): tie the check to
#91's sidecar path resolution so it knows WHICH file to inspect. Mirror the
sidecar-warnings shape.
GUI: a warning label + "Fix permissions" button in the stdio editor (mirrors the
removed-flag surface), shown only when the sidecar exists and is too open. Hidden
on Windows and for non-sidecar servers. Smoke-tested headlessly.
pytest green (529 passed), ruff + format clean. Closes#93. Part of epic #94.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
permission_status/fix_permissions wrap paths in the running OS's Path class, so
on the windows-latest CI job str(p) uses backslashes and the fixture dict lookups
miss — the same portability trap fixed for #91. Normalise with Path(p).as_posix()
in the four affected lambdas. Pure test fix; production code unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #90. Closes #91. Closes #92. Closes #93.
Verified integration of the overnight PRs #96/#97/#98/#99 into one landing PR. Supersede + close those four on merge.
Why one PR instead of merging the four
#98 (#92, version) was branched as a sibling of #97 (#91, sidecar) — both off #90 — rather than stacked on it. So merging them in sequence conflicts in
bcc_core.py+tests/test_core.py(each just adds an independent section at the same anchor; each PR is green alone, so CI didn't surface it). Resolved as a union (keep both sections/tests) + a two-blank-lineruff formattouch-up. #93 then merges clean. The clean chain was 90→91→93; #92 was the odd one.Verified on the integrated tree (all four together)
python -m pytest— green, 1 skipped (509→529 passing).ruff check .+ruff format --check .— clean.python -m py_compile bcc.py— OK.FLAG_ENV_MIGRATIONSis now a derived view ofSERVER_SPECS; all #89 migration tests pass unchanged, and--sudoPassword/--suPasswordnow auto-migrate toSSH_MCP_SUDO_PASSWORDas intended (one #89 test updated intentionally for that data growth).apply_serversstill only writesmcpServers/_disabledMcpServers. No network. Onlybcc_core.py/bcc.py/tests/test_core.pytouched.Contents
ServerSpecdataclass +SERVER_SPECSregistry (env/removed/drift flags, sidecar paths, schema);FLAG_ENV_MIGRATIONSderived from it; sudo/su +--maxChars=nonedrift added.mode & 0o077check (file 0600 / dir 0700, POSIX-only) + "Fix permissions" button.Reviewer caveats (from the overnight report, confirmed)
config.toml): #91 args-inert + wrong-path lines; #92 version badge + "Pin to<v>"; #93 permission warning + "Fix permissions"; and that all new rows hide for remote servers / no selection.count_toml_profilesis a heuristic (counts TOML headers; ssh-mcp's real multi-profile schema wasn't verifiable). Advisory-only — never edits/blocks. Don't let it drive anything stronger until confirmed.chmods the sidecar — the only new action that writes to the filesystem (on explicit click); everything else is read-only.Do not merge until the GUI click-throughs pass.
ssh-mcp v2 reads a TOML sidecar and only falls back to CLI args when that file is ABSENT — so BCC's managed --host/--user args can be completely inert while the real config lives in a file BCC never looks at. This adds read-only truth-telling for that (no sidecar writing). Core (pure, fully injectable platform/environ/home/exists/read for testing): - sidecar_path(spec): VERIFIED per-platform TOML location from the package source, NOT the README (macOS → ~/Library/Application Support/ssh-mcp, Windows → %APPDATA%, else → ${XDG_CONFIG_HOME:-~/.config}). sidecar_doc_path() is the README path. - sidecar_status(): resolves exists / has_managed_args / args_inert / wrong_path. - sidecar_warnings(): mirrors removed_flag_warnings' shape. Reports: * precedence — "these arguments are inert; the server reads <real path>" * wrong-path — a TOML at the README path the server never actually reads * #11 credential scoping — an unprefixed SSH_MCP_PASSWORD shared across 2+ profiles in a multi-profile sidecar (count_toml_profiles is a documented 3.10-safe heuristic; unscoped_credential_warning gates on it). GUI: a read-only advisory label in the stdio editor (mirrors the removed-flag label; no fix button — editing the sidecar is a separate deliberate action). Uses the real platform/env/filesystem so it reflects this machine. pytest green, ruff + format clean. Closes #91. Part of epic #94. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`npx -y ssh-mcp` resolves *latest* on every launch — in one session ssh-mcp went v1 → v2 and the tool set changed under a running agent, mid-task, with no warning. This detects that and turns it into a comprehensible pin/upgrade prompt. Core (pure, no network — reuses parse_version / is_newer_version / the catalog spec parsers; the resolved-version lookup is fully injectable for tests): - server_package_spec / server_package_name — the npm spec an npx-style server runs. - is_unpinned_spec — bare name or dist-tag (@latest/@next) is unpinned; an exact numeric version is pinned. - resolved_npx_version — reads the local ~/.npm/_npx cache (highest version wins), degrades to None cleanly. NO network. find/read injectable. - pin_spec_transform — rewrite the spec to name@version (mirrors pin_command_path's (new_data, note) contract). No-op when already pinned / bad version / not npx. - version_drift_note — "moved X → Y since you pinned" via numeric is_newer_version. - version_status — the badge's high-level dict (unpinned / pinned_version / resolved_version / can_pin / drift). A _UNSET sentinel lets callers force an explicit resolved=None ("unknown") vs. omitting it to do the local lookup. GUI: a version badge row under the dependency status (mirrors that surface) with a one-click "Pin to <version>" button, shown only for npx servers; a drift note when a pinned version has been overtaken. Smoke-tested headlessly. pytest green (520 passed), ruff + format clean. Closes #92. Part of epic #94. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>The CI matrix includes a windows-latest job where str(Path("/Users/…")) renders with backslashes, so the platform-parameterised sidecar-path assertions failed there (macOS/Linux passed). Compare .as_posix() instead — separator-normalised and portable — and note why. Pure test fix; no production-code change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>