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
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
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e087107710
commit
8c51c25211
@@ -3297,6 +3297,130 @@ def test_drift_warning_quiet_for_other_values_and_unknown_pkg():
|
||||
assert c.drift_warnings({"command": "npx", "args": ["other", "--maxChars=none"]}) == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Sidecar config detection + precedence (issue #91)
|
||||
# --------------------------------------------------------------------------- #
|
||||
_SSH = {"command": "npx", "args": ["-y", "ssh-mcp", "--host=h", "--user=u"]}
|
||||
_HOME = "/Users/tester"
|
||||
|
||||
|
||||
def test_sidecar_path_verified_per_platform():
|
||||
spec = c.SERVER_SPECS["ssh-mcp"]
|
||||
mac = c.sidecar_path(spec, platform="darwin", environ={}, home=_HOME)
|
||||
assert str(mac) == "/Users/tester/Library/Application Support/ssh-mcp/config.toml"
|
||||
# Windows resolves under %APPDATA%, not ~/.config.
|
||||
win = c.sidecar_path(
|
||||
spec, platform="win32", environ={"APPDATA": "C:/Users/t/AppData/Roaming"}, home=_HOME
|
||||
)
|
||||
assert "ssh-mcp/config.toml" in str(win)
|
||||
assert "AppData/Roaming" in str(win)
|
||||
# POSIX honours XDG_CONFIG_HOME, else ~/.config.
|
||||
xdg = c.sidecar_path(spec, platform="linux", environ={"XDG_CONFIG_HOME": "/cfg"}, home=_HOME)
|
||||
assert str(xdg) == "/cfg/ssh-mcp/config.toml"
|
||||
default = c.sidecar_path(spec, platform="linux", environ={}, home=_HOME)
|
||||
assert str(default) == "/Users/tester/.config/ssh-mcp/config.toml"
|
||||
|
||||
|
||||
def test_sidecar_path_none_for_non_sidecar_package():
|
||||
assert c.sidecar_path(None) is None
|
||||
assert c.sidecar_path(c.ServerSpec(package="nope")) is None
|
||||
|
||||
|
||||
def test_sidecar_args_inert_when_file_exists():
|
||||
real = c.sidecar_path(c.SERVER_SPECS["ssh-mcp"], platform="darwin", environ={}, home=_HOME)
|
||||
st = c.sidecar_status(
|
||||
_SSH, platform="darwin", environ={}, home=_HOME, exists=lambda p: p == real
|
||||
)
|
||||
assert st["exists"] is True
|
||||
assert st["has_managed_args"] is True
|
||||
assert st["args_inert"] is True
|
||||
assert st["wrong_path"] is False
|
||||
warns = c.sidecar_warnings(
|
||||
_SSH, platform="darwin", environ={}, home=_HOME, exists=lambda p: p == real
|
||||
)
|
||||
assert warns and "inert" in warns[0]
|
||||
assert str(real) in warns[0]
|
||||
|
||||
|
||||
def test_sidecar_args_live_when_file_absent():
|
||||
st = c.sidecar_status(_SSH, platform="darwin", environ={}, home=_HOME, exists=lambda p: False)
|
||||
assert st["exists"] is False
|
||||
assert st["args_inert"] is False
|
||||
assert (
|
||||
c.sidecar_warnings(_SSH, platform="darwin", environ={}, home=_HOME, exists=lambda p: False)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_sidecar_wrong_path_flag_on_macos():
|
||||
# A TOML written at the README's ~/.config path is never read on macOS.
|
||||
doc = c.sidecar_doc_path(c.SERVER_SPECS["ssh-mcp"], home=_HOME)
|
||||
assert str(doc) == "/Users/tester/.config/ssh-mcp/config.toml"
|
||||
st = c.sidecar_status(
|
||||
_SSH, platform="darwin", environ={}, home=_HOME, exists=lambda p: p == doc
|
||||
)
|
||||
assert st["wrong_path"] is True
|
||||
assert st["args_inert"] is False # the real file doesn't exist, so args still apply
|
||||
warns = c.sidecar_warnings(
|
||||
_SSH, platform="darwin", environ={}, home=_HOME, exists=lambda p: p == doc
|
||||
)
|
||||
assert warns and "never loaded" in warns[0]
|
||||
|
||||
|
||||
def test_sidecar_no_wrong_path_on_linux_where_paths_coincide():
|
||||
# On Linux the real path and the doc path are the same, so a file there is
|
||||
# correctly loaded — no wrong-path warning.
|
||||
real = c.sidecar_path(c.SERVER_SPECS["ssh-mcp"], platform="linux", environ={}, home=_HOME)
|
||||
st = c.sidecar_status(
|
||||
_SSH, platform="linux", environ={}, home=_HOME, exists=lambda p: p == real
|
||||
)
|
||||
assert st["wrong_path"] is False
|
||||
assert st["args_inert"] is True
|
||||
|
||||
|
||||
def test_count_toml_profiles():
|
||||
assert c.count_toml_profiles("") == 0
|
||||
assert c.count_toml_profiles("[server]\nhost='h'\n") == 1
|
||||
text = "# comment\n[[hosts]]\nname='a'\n[[hosts]]\nname='b'\n[settings]\nx=1\n"
|
||||
# two [[hosts]] share a top-level name -> one profile group; [settings] -> another.
|
||||
assert c.count_toml_profiles(text) == 2
|
||||
multi = "[prod]\nhost='p'\n[prod.auth]\nkey='k'\n[staging]\nhost='s'\n"
|
||||
assert c.count_toml_profiles(multi) == 2
|
||||
|
||||
|
||||
def test_unscoped_credential_warning_11():
|
||||
data = dict(_SSH, env={"SSH_MCP_PASSWORD": "shared"})
|
||||
# Single profile: no sharing concern.
|
||||
assert c.unscoped_credential_warning(data, profile_count=1) is None
|
||||
# Unknown count: claim nothing.
|
||||
assert c.unscoped_credential_warning(data, profile_count=None) is None
|
||||
# Multiple profiles + bare credential: warn.
|
||||
w = c.unscoped_credential_warning(data, profile_count=3)
|
||||
assert w and "every one of the 3 profiles" in w
|
||||
# No bare credential: nothing to warn.
|
||||
assert c.unscoped_credential_warning(_SSH, profile_count=3) is None
|
||||
|
||||
|
||||
def test_sidecar_warnings_includes_credential_scoping():
|
||||
real = c.sidecar_path(c.SERVER_SPECS["ssh-mcp"], platform="darwin", environ={}, home=_HOME)
|
||||
data = dict(_SSH, env={"SSH_MCP_PASSWORD": "shared"})
|
||||
toml = "[prod]\nhost='p'\n[staging]\nhost='s'\n"
|
||||
warns = c.sidecar_warnings(
|
||||
data,
|
||||
platform="darwin",
|
||||
environ={},
|
||||
home=_HOME,
|
||||
exists=lambda p: p == real,
|
||||
read_text=lambda p: toml,
|
||||
)
|
||||
assert any("profiles" in w for w in warns)
|
||||
|
||||
|
||||
def test_sidecar_status_none_for_unknown_package():
|
||||
assert c.sidecar_status({"command": "npx", "args": ["other"]}) is None
|
||||
assert c.sidecar_warnings({"command": "npx", "args": ["other"]}) == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Move to environment variable (issue #83)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user