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
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
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2e5d0351b4
commit
0b2827e6b8
@@ -3424,6 +3424,116 @@ def test_sidecar_status_none_for_unknown_package():
|
||||
assert c.sidecar_warnings({"command": "npx", "args": ["other"]}) == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Filesystem permission pre-flight (issue #93)
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_permission_status_ok_when_0600():
|
||||
st = c.permission_status(
|
||||
"/cfg/config.toml",
|
||||
platform="darwin",
|
||||
stat_mode=lambda p: {"/cfg/config.toml": 0o600, "/cfg": 0o700}.get(str(p)),
|
||||
)
|
||||
assert st["ok"] is True
|
||||
assert st["mode"] == 0o600
|
||||
assert st["problems"] == []
|
||||
|
||||
|
||||
def test_permission_status_blocks_group_world_readable_file():
|
||||
st = c.permission_status(
|
||||
"/cfg/config.toml",
|
||||
platform="linux",
|
||||
stat_mode=lambda p: {"/cfg/config.toml": 0o644, "/cfg": 0o755}.get(str(p)),
|
||||
)
|
||||
assert st["ok"] is False
|
||||
assert st["file_ok"] is False
|
||||
assert st["dir_ok"] is False
|
||||
# Two plain-language problems, naming the offending octal modes.
|
||||
text = " ".join(st["problems"])
|
||||
assert "0644" in text and "0600" in text
|
||||
assert "0755" in text and "0700" in text
|
||||
|
||||
|
||||
def test_permission_status_file_bad_dir_ok():
|
||||
st = c.permission_status(
|
||||
"/cfg/config.toml",
|
||||
platform="linux",
|
||||
stat_mode=lambda p: {"/cfg/config.toml": 0o640, "/cfg": 0o700}.get(str(p)),
|
||||
)
|
||||
assert st["file_ok"] is False
|
||||
assert st["dir_ok"] is True
|
||||
assert len(st["problems"]) == 1
|
||||
|
||||
|
||||
def test_permission_status_none_on_windows_and_missing_file():
|
||||
# Windows: POSIX modes don't apply -> None (clean no-op).
|
||||
assert c.permission_status("/cfg/config.toml", platform="win32") is None
|
||||
# Absent file -> nothing to pre-flight.
|
||||
assert c.permission_status("/cfg/gone.toml", platform="linux", stat_mode=lambda p: None) is None
|
||||
|
||||
|
||||
def test_fix_permissions_chmods_file_and_dir():
|
||||
calls = []
|
||||
changed, note = c.fix_permissions(
|
||||
"/cfg/config.toml", platform="linux", chmod=lambda p, m: calls.append((str(p), m))
|
||||
)
|
||||
assert changed is True
|
||||
assert ("/cfg/config.toml", 0o600) in calls
|
||||
assert ("/cfg", 0o700) in calls
|
||||
assert note and "0600" in note
|
||||
|
||||
|
||||
def test_fix_permissions_noop_on_windows():
|
||||
calls = []
|
||||
changed, note = c.fix_permissions(
|
||||
"/cfg/config.toml", platform="win32", chmod=lambda p, m: calls.append((p, m))
|
||||
)
|
||||
assert changed is False
|
||||
assert note is None
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_fix_permissions_reports_oserror():
|
||||
def boom(p, m):
|
||||
raise OSError("nope")
|
||||
|
||||
changed, note = c.fix_permissions("/cfg/config.toml", platform="linux", chmod=boom)
|
||||
assert changed is False
|
||||
assert "could not change permissions" in note
|
||||
|
||||
|
||||
def test_sidecar_permission_warnings_over_real_path():
|
||||
data = {"command": "npx", "args": ["-y", "ssh-mcp", "--host=h"]}
|
||||
real = c.sidecar_path(c.SERVER_SPECS["ssh-mcp"], platform="darwin", environ={}, home="/Users/t")
|
||||
|
||||
def stat_mode(p):
|
||||
return 0o644 if Path(p) == real else 0o700
|
||||
|
||||
warns = c.sidecar_permission_warnings(
|
||||
data, platform="darwin", environ={}, home="/Users/t", stat_mode=stat_mode
|
||||
)
|
||||
assert warns and warns[0].startswith("ssh-mcp:")
|
||||
assert "0600" in warns[0]
|
||||
# Windows / unknown package -> nothing.
|
||||
assert c.sidecar_permission_warnings(data, platform="win32") == []
|
||||
assert c.sidecar_permission_warnings({"command": "npx", "args": ["other"]}) == []
|
||||
|
||||
|
||||
def test_sidecar_permission_warnings_quiet_when_tight():
|
||||
data = {"command": "npx", "args": ["-y", "ssh-mcp", "--host=h"]}
|
||||
warns = c.sidecar_permission_warnings(
|
||||
data, platform="darwin", environ={}, home="/Users/t", stat_mode=lambda p: 0o600
|
||||
)
|
||||
assert warns == []
|
||||
|
||||
|
||||
def test_sidecar_permission_fix_target():
|
||||
data = {"command": "npx", "args": ["-y", "ssh-mcp"]}
|
||||
tgt = c.sidecar_permission_fix_target(data, platform="darwin", environ={}, home="/Users/t")
|
||||
assert tgt is not None and tgt.name == "config.toml"
|
||||
assert c.sidecar_permission_fix_target(data, platform="win32") is None
|
||||
assert c.sidecar_permission_fix_target({"command": "npx", "args": ["other"]}) is None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Move to environment variable (issue #83)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user