"""Left pane: drive zerto_rewind_mcp over real MCP stdio. Full rewind loop.""" from __future__ import annotations import asyncio, json, os, shlex, subprocess, sys, time from pathlib import Path from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client SP = Path(__file__).parent CFG = json.loads((SP / "demo.json").read_text()) REPO = "/home/justin/github/zerto-ai-rewind" RST, BOLD, DIM = "\033[0m", "\033[1m", "\033[2m" CYAN, GREEN, RED, YEL, MAG = "\033[36m", "\033[32m", "\033[31m", "\033[33m", "\033[35m" SPEED = float(os.environ.get("DEMO_SPEED", "1.0")) def recovered_bytes(rec: dict) -> bytes: """zerto_recover_file returns the content now, not a path on the MCP host. The server may be on another machine, so a path there is of no use here. """ import base64 if rec.get("encoding") == "base64": return base64.b64decode(rec["content"]) return rec["content"].encode() def w(s=""): sys.stdout.write(s + "\n"); sys.stdout.flush() def type_out(s, delay=0.012, color=""): sys.stdout.write(color) for ch in s: sys.stdout.write(ch); sys.stdout.flush() time.sleep(delay * SPEED) sys.stdout.write(RST + "\n"); sys.stdout.flush() def beat(n=1.2): time.sleep(n * SPEED) STEP = [0] def step(title): STEP[0] += 1 w() w(f"{BOLD}{CYAN}{'='*66}{RST}") type_out(f" STEP {STEP[0]} {title}", 0.008, BOLD + CYAN) w(f"{BOLD}{CYAN}{'='*66}{RST}") beat(0.5) def note(s): type_out(f" {s}", 0.010, DIM) def call_banner(tool, args): w(f"{MAG} -> MCP call{RST} {BOLD}{tool}{RST}") for k, v in args.items(): w(f"{DIM} {k} = {v}{RST}") def show(payload, keep=None, limit=22): if keep: payload = {k: payload[k] for k in keep if k in payload} text = json.dumps(payload, indent=2, default=str) lines = text.splitlines() for line in lines[:limit]: w(f"{DIM} |{RST} {line}") if len(lines) > limit: w(f"{DIM} | ... {len(lines)-limit} more lines{RST}") def ssh_base(): if CFG.get("password"): return ["sshpass", "-p", CFG["password"], "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR"] base = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR"] if CFG.get("key"): base += ["-i", CFG["key"]] return base def guest(cmd, show_cmd=True): target = f"{CFG['user']}@{CFG['host']}" full = ssh_base() + [target, cmd] if show_cmd: w(f"{YEL} $ ssh {target} {shlex.quote(cmd)}{RST}") r = subprocess.run(full, capture_output=True, text=True, timeout=60) out = (r.stdout + r.stderr).rstrip() for line in out.splitlines(): w(f"{DIM} |{RST} {line}") return r.returncode, out async def main(): w(f"\n{BOLD}{CYAN} ZERTO AI REWIND{RST} {DIM}live end-to-end{RST}\n") type_out(" An agent is about to change a file on a protected VM.", 0.014) type_out(" Zerto already journals that VM. Make the agent use it.", 0.014) beat(1.5) params = StdioServerParameters( command=f"{REPO}/.venv/bin/zerto-rewind-mcp", env={**os.environ, "ZERTO_REWIND_CONFIG": f"{REPO}/config.json"}, ) devnull = open(os.devnull, 'w') async with stdio_client(params, errlog=devnull) as (r, s): async with ClientSession(r, s) as sess: step("Connect to the rewind MCP") await sess.initialize() tools = await sess.list_tools() note(f"{len(tools.tools)} tools from zerto_rewind_mcp") for t in tools.tools: w(f"{DIM} - {t.name}{RST}") beat(2.0) async def _ticker(label): t0 = time.time() try: while True: await asyncio.sleep(5) el = int(time.time() - t0) sys.stdout.write(f"\r{DIM} {label} ... {el}s{RST}") sys.stdout.flush() except asyncio.CancelledError: el = int(time.time() - t0) if el >= 5: sys.stdout.write(f"\r{DIM} {label} ... {el}s done{RST}\n") else: sys.stdout.write("\r" + " " * 50 + "\r") sys.stdout.flush() raise async def call(name, **args): call_banner(name, args) tick = asyncio.create_task(_ticker("working")) try: res = await sess.call_tool(name, args) finally: tick.cancel() try: await tick except asyncio.CancelledError: pass return json.loads(res.content[0].text) # ---- 1. the good state on the guest step("The file the agent is about to break") guest(f"cat {CFG['guest_path']}") beat(1.8) # ---- 2. find protection step("zerto_find_protection - is this VM protected?") note("Reads skip the guard. This is the read.") found = await call("zerto_find_protection", query=CFG["vm_query"]) show(found) if not found.get("ok"): w(f"{RED} find_protection not ok - stopping{RST}"); return 1 vm = found["vm"] beat(2.5) # ---- 3. guard step("zerto_guard_before_mutate - pin the journal FIRST") note("Capture-before-execute. No tag, no change.") note("The checkpoint name records which agent, and what it is about to do.") note("Watch the journal pane on the right.") guard = await call("zerto_guard_before_mutate", query=CFG["vm_query"], change_id=CFG["change_id"], agent="claude", action=f"edit {CFG['guest_path']}") show(guard, keep=["ok", "tag", "tagged", "skipped", "errors", "message"]) if not guard.get("ok"): w(f"{RED} GUARD FAILED - refusing the change{RST}"); return 1 tag = guard["tag"] tagged = guard["tagged"][0] t_tag = time.strftime("%H:%M:%S") w(f"{GREEN}{BOLD} tagged checkpoint {tagged['checkpoint_id']} on {tagged['vpg_name']}{RST}") w(f"{GREEN} inserted at {t_tag}{RST}") w(f"{GREEN} name: {tag}{RST}") w(f"{DIM} this checkpoint is the rewind point. remember cp {tagged['checkpoint_id']}.{RST}") beat(4.0) # ---- 4. the bad change step("Now the agent makes the bad change") guest(f"printf '%s\\n' {shlex.quote(CFG['bad_content'])} > {CFG['guest_path']}") guest(f"cat {CFG['guest_path']}") t_bad = time.strftime("%H:%M:%S") w(f"{RED}{BOLD} the good content is gone from the guest ({t_bad}){RST}") note("Git never had this file. Last night's backup is hours stale.") beat(2.5) # ---- 5. refusal without a human step("zerto_recover_file without a human yes") deny = await call("zerto_recover_file", vpg_identifier=tagged["vpg_identifier"], vm_identifier=vm["vm_identifier"], checkpoint_identifier=tagged["checkpoint_id"], guest_path=CFG["guest_path"], confirmed=False) show(deny) w(f"{YEL} refused. FLR mounts a disk; a human says yes.{RST}") beat(2.5) # ---- 6. recover rec_cp = tagged["checkpoint_id"] step("Human says yes - FLR from the pre-mutation checkpoint") w(f"{BOLD} provenance{RST}") w(f" {GREEN}cp {tagged['checkpoint_id']} inserted {t_tag}{RST} {DIM}<- guard, BEFORE the change{RST}") w(f" {RED}bad write {t_bad}{RST} {DIM}<- the mutation, AFTER{RST}") assert rec_cp == tagged["checkpoint_id"], "recovery must use the guard checkpoint" w(f"{DIM} recovering from cp {rec_cp}, not the newest checkpoint.{RST}") note(f"name: {tag}") rec = await call("zerto_recover_file", vpg_identifier=tagged["vpg_identifier"], vm_identifier=vm["vm_identifier"], checkpoint_identifier=tagged["checkpoint_id"], guest_path=CFG["guest_path"], confirmed=True) show(rec) if not rec.get("ok"): w(f"{RED} FLR failed{RST}"); return 1 beat(1.5) w(f"{YEL} recovered {rec['bytes']} bytes, sha256 {rec['sha256'][:16]}...{RST}") body = recovered_bytes(rec).decode() for line in body.splitlines(): w(f"{GREEN}{BOLD} | {line}{RST}") beat(2.5) # ---- 7. put it back step("Copy it back to the guest") note("This part is scp, not Zerto. Zerto got the bytes back.") target = f"{CFG['user']}@{CFG['host']}" local_copy = SP / rec["name"] local_copy.write_bytes(recovered_bytes(rec)) scp = (["sshpass", "-p", CFG["password"]] if CFG.get("password") else []) + \ ["scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR"] + (["-i", CFG["key"]] if CFG.get("key") else []) + \ [str(local_copy), f"{target}:{CFG['guest_path']}"] w(f"{YEL} $ scp {rec['name']} {target}:{CFG['guest_path']}{RST}") subprocess.run(scp, capture_output=True, text=True, timeout=60) guest(f"cat {CFG['guest_path']}") beat(1.5) w() w(f"{BOLD}{GREEN}{'='*66}{RST}") type_out(" REWOUND. RPO was the journal, not last night.", 0.016, BOLD + GREEN) w(f"{BOLD}{GREEN}{'='*66}{RST}") beat(4.0) return 0 sys.exit(asyncio.run(main()))