feat(catalog): cover Windows guest-mutating tools in the starter list

The catalog is opt-in: an unlisted tool passes through unguarded. The
shipped starter list was ssh/exec and ansible/run_playbook, both Linux
shaped, so an agent changing a protected Windows guest over WinRM or
PowerShell was never guarded at all. That does not fail loudly, it
simply never inserts a checkpoint.

Adds winrm/run_command, winrm/run_ps, powershell/invoke_command and
smb/write_file. The smb entry is there because a file written into a
share changes the guest without any shell being involved.

Test asserts the example config covers both platforms and that every
entry names a vm_arg, since without one the guard cannot resolve a VM.

Still illustrative, not exhaustive: tool names vary per MCP server, so
users add their own with zerto_add_mutating_tool. That reactive model is
the real weakness here and is worth revisiting separately.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
This commit is contained in:
2026-09-21 14:32:52 -04:00
co-authored by Claude Opus 5
parent 90768f17e1
commit e5f05eff6c
3 changed files with 44 additions and 1 deletions
+19
View File
@@ -31,3 +31,22 @@ def test_entry_requires_fields():
raise AssertionError("expected ValueError")
except ValueError:
pass
def test_example_config_covers_windows_and_linux():
"""The starter catalog must not be Linux-only.
The catalog is opt-in: an unlisted tool passes through unguarded. A
Windows-only shop taking the shipped defaults would therefore mutate
protected guests with no checkpoint at all.
"""
example = Path(__file__).resolve().parent.parent / "config.example.json"
data = json.loads(example.read_text(encoding="utf-8"))
cat = MutatingCatalog.from_config(data)
assert cat.get("ssh", "exec") is not None
assert cat.get("winrm", "run_command") is not None
assert cat.get("winrm", "run_ps") is not None
assert cat.get("powershell", "invoke_command") is not None
# every entry must name the arg holding the VM, or the guard cannot resolve one
for entry in cat.list():
assert entry.vm_arg, f"{entry.server}/{entry.tool} has no vm_arg"