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
+1 -1
View File
@@ -9,7 +9,7 @@ If the loop works, these tools are the delta to add to official ZVM MCP (`ZVM.MC
1. `zerto_find_protection` — VM name, hostname, or vmIdentifier to exactly one VM and every VPG. Zero or two-plus VMs: stop. 1. `zerto_find_protection` — VM name, hostname, or vmIdentifier to exactly one VM and every VPG. Zero or two-plus VMs: stop.
2. `zerto_create_tagged_checkpoint` / `zerto_guard_before_mutate` — same tag on every protecting VPG, wait until listed. The name records which agent and what it is doing: `ai:<agent> | <action> | vm=<vm> | change=<id> | <utc>`. 2. `zerto_create_tagged_checkpoint` / `zerto_guard_before_mutate` — same tag on every protecting VPG, wait until listed. The name records which agent and what it is doing: `ai:<agent> | <action> | vm=<vm> | change=<id> | <utc>`.
3. `zerto_recover_file` — FLR after a human sets `confirmed=true`. Linux and Windows guest paths. Locally replicated VPGs only: FLR runs at the VPG's recovery site. Reports its own unmount; `zerto_list_flr_sessions` / `zerto_end_flr_session` find and reap a mount orphaned by a crashed recovery. 3. `zerto_recover_file` — FLR after a human sets `confirmed=true`. Linux and Windows guest paths. Locally replicated VPGs only: FLR runs at the VPG's recovery site. Reports its own unmount; `zerto_list_flr_sessions` / `zerto_end_flr_session` find and reap a mount orphaned by a crashed recovery.
4. Mutating catalog — opt-in list of MCP tools that must be guarded. Unlisted tools pass through. Users add entries. 4. Mutating catalog — opt-in list of MCP tools that must be guarded. Unlisted tools pass through. Users add entries. The starter list covers Linux (`ssh`, `ansible`) and Windows (`winrm`, `powershell`, `smb`), and is illustrative, not exhaustive.
Official ZVM MCP already has inventory and failover test. It does not insert tagged checkpoints or run FLR. Official ZVM MCP already has inventory and failover test. It does not insert tagged checkpoints or run FLR.
+24
View File
@@ -17,6 +17,30 @@
"tool": "run_playbook", "tool": "run_playbook",
"vm_arg": "limit", "vm_arg": "limit",
"notes": "Playbook target host/group. Resolve to a single VM before guard." "notes": "Playbook target host/group. Resolve to a single VM before guard."
},
{
"server": "winrm",
"tool": "run_command",
"vm_arg": "host",
"notes": "Windows remote shell over WinRM. vm_arg is the hostname."
},
{
"server": "winrm",
"tool": "run_ps",
"vm_arg": "host",
"notes": "PowerShell over WinRM. Same blast radius as run_command."
},
{
"server": "powershell",
"tool": "invoke_command",
"vm_arg": "computer_name",
"notes": "Invoke-Command against a remote Windows guest."
},
{
"server": "smb",
"tool": "write_file",
"vm_arg": "host",
"notes": "Writes a file into a Windows share. Changes the guest without a shell."
} }
] ]
} }
+19
View File
@@ -31,3 +31,22 @@ def test_entry_requires_fields():
raise AssertionError("expected ValueError") raise AssertionError("expected ValueError")
except ValueError: except ValueError:
pass 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"