Land ServerSpec cut: spine + sidecar + version + perms (#90–#93) #100

Merged
the_og merged 9 commits from feat/serverspec-cut into main 2026-08-12 23:52:59 -04:00
Owner

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" 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.
the_og added 9 commits 2026-08-12 10:21:23 -04:00
feat(#90): promote FLAG_ENV_MIGRATIONS into a ServerSpec spine
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 25s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 11s
CI / Catalog signature (pull_request) Successful in 6s
e087107710
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>
feat(#91): sidecar config detection + precedence + verified paths
CI / Tests (py3.12 / windows-latest) (pull_request) Failing after 23s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 46s
CI / Catalog signature (pull_request) Successful in 8s
CI / Lint (ruff) (pull_request) Successful in 36s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 15s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 16s
8c51c25211
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>
feat(#92): detect unpinned npx specs, resolve version, one-click pin, drift
CI / Lint (ruff) (pull_request) Successful in 9s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 21s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 13s
CI / Catalog signature (pull_request) Successful in 9s
2cd8e0fb3b
`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>
test(#91): assert sidecar paths via as_posix() for the Windows CI runner
CI / Lint (ruff) (pull_request) Successful in 9s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 22s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 15s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 13s
CI / Catalog signature (pull_request) Successful in 8s
2e5d0351b4
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>
feat(#93): filesystem permission pre-flight for credential configs (0600/0700)
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 13s
CI / Lint (ruff) (pull_request) Successful in 8s
CI / Tests (py3.12 / windows-latest) (pull_request) Failing after 24s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 12s
CI / Catalog signature (pull_request) Successful in 8s
0b2827e6b8
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>
test(#93): key injected mode/chmod maps on as_posix() for the Windows runner
CI / Lint (ruff) (pull_request) Successful in 9s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 23s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 16s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 13s
CI / Catalog signature (pull_request) Successful in 8s
603d24566d
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>
# Conflicts:
#	bcc_core.py
#	tests/test_core.py
style: ruff format the union-merge seam
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 35s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 11s
CI / Catalog signature (pull_request) Successful in 7s
0920846c2c
the_og merged commit cdda60da1b into main 2026-08-12 23:52:59 -04:00
the_og deleted branch feat/serverspec-cut 2026-08-12 23:53:00 -04:00
Sign in to join this conversation.