Compare commits

..
2 Commits
Author SHA1 Message Date
justinandClaude Opus 5 735c6c4a64 docs(hooks): correct the cloud-protected denial claim
hooks/README.md said the win2019-1 denial happened because Zerto cannot
insert a tagged checkpoint on an AWS-protected VPG. That is not what
happened, and it is not true on 10.9.10.

The insert succeeded. The checkpoint is in the journal as cp 56, stamped
about a minute after the hook had already reported failure. What actually
happened is that wait_for_tag gave up after its hardcoded 45s, which is
generous on a VPG that checkpoints every 5s and far too short on one that
checkpoints every 630s.

So the documented "verified" example was a false denial. The deny path
works; that particular denial was wrong. Recorded as a known issue,
because a guard that silently refuses legitimate work on every
cloud-protected VM while looking correct is a worse failure than the one
it exists to prevent.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
2026-09-22 18:05:23 -04:00
justinandClaude Opus 5 50cd68fa10 feat(hooks): enforce the guard from a PreToolUse hook
The catalog and zerto_check_tool are advice. An MCP server cannot see or
block another server's tool calls, so a model that skips the guard is not
stopped by anything, and the change lands with no checkpoint behind it.

A PreToolUse hook runs in the host, where the tool call actually pauses.
That turns capture-before-execute from a convention into something the
host enforces.

  read-only                      no decision, runs
  mutating, checkpoint confirmed no decision, plus additionalContext
                                 telling the model which checkpoint to
                                 recover from
  mutating, checkpoint failed    DENY, the call never happens
  mutating, VM unknown to Zerto  prompt, nothing to rewind to
  mutating, no VM in the args    prompt, the catalog's vm_arg missed
  unlisted tool                  prompt

Decisions go out as JSON rather than exit 2. Exit 2 blocks
unconditionally but discards the JSON, and with it the reason, so the
model would be refused without being told why.

A lookup miss is not a refusal. Asking Zerto for a plain hostname by
vmIdentifier returns HTTP 400, and an early version reported that as
"could not tag a checkpoint", which denied changes to machines Zerto had
simply never heard of. Those are now separated: unknown VM prompts, a VM
Zerto knows but will not tag denies.

Timeouts are budgeted for the slow path. A successful tag took about 4s,
but a refusal took 63s, because wait_for_tag spends 45s waiting for a
checkpoint that will never arrive on an AWS or Azure protected VPG. If
the host's timeout fires first it cancels the hook and discards its
output, and the call proceeds unguarded, so the hook's own budget (150s)
stays under the configured one (180s): better to deny than be cancelled.

Verified against a live ZVM 10.9.10. jp-ubuntu tagged checkpoint 7180 and
was allowed; win2019-1, whose VPG is protected at an AWS site where
tagged checkpoints are unsupported, was denied. Both held under
permission_mode bypassPermissions, which is when an agent is most likely
running unattended.

Broad excepts in hooks/ are deliberate and scoped in pyproject: a hook
that raises breaks the tool call it exists to protect.

pytest 60 passed (12 new).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
2026-09-22 08:42:37 -04:00
6 changed files with 9 additions and 29 deletions
+1 -16
View File
@@ -64,22 +64,7 @@ Git never had the file. RPO is the journal, not last night's backup.
vSphere ZVM 10.x and ZCA on AWS/Azure, same REST paths. HVM is out (separate swagger). Failover Live is not a tool.
Tagged checkpoints **do** work when the protected site is Azure or AWS. The 9.0 API
reference says they cannot be inserted; that is wrong on 10.9.10, where both were
accepted and the task reached `Completed`.
What differs is latency and granularity, both set by the **protected** site:
| protected at | journal gap | tag visible after |
|---|---|---|
| vSphere | 5s | ~4s |
| Azure | 60s | ~34s |
| AWS | 630s | ~128s |
The tagged checkpoint is also stamped about 30s *after* the insert request, so on a
cloud-protected VPG a prompt mutation can land *inside* the checkpoint meant to
precede it. Recover from the newest checkpoint that already existed when the guard
ran, not from the tag.
Tagged checkpoints cannot be inserted when the **protected** site is Azure or AWS (Zerto API). Point this server at the vSphere protected ZVM.
## Not this product
+1 -4
View File
@@ -70,10 +70,7 @@ so a stuck session blocks the next recovery. Find it with
## Facts that bite
- A tagged checkpoint is crash-consistent, not app-quiesced.
- Tagged checkpoints work on Azure and AWS protected VPGs, but they appear late:
~34s (Azure) and ~128s (AWS) versus ~4s on vSphere, and the checkpoint is stamped
about 30s after you ask for it. On those VPGs the tag can end up *after* your
change, so treat the newest checkpoint that already existed as the rewind point.
- Tagged checkpoints are not supported when the **protected** site is Azure or AWS. Talk to the vSphere protected ZVM.
- 10.9 FLR Operator RBAC fails; Administrator is the documented workaround.
- FLR cannot run during clone, test, live failover, or EJC.
- Linux FLR: files >1.5GB are a bad idea; some characters in names are refused.
+1 -4
View File
@@ -93,10 +93,7 @@ async def wait_for_tag(
raise ZertoError(
f"Tagged checkpoint {tag!r} did not appear on VPG {vpg_identifier} "
f"within {timeout_s:.0f}s. Do not mutate. "
"On a cloud-protected VPG the tag routinely takes longer than this to appear "
"(measured ~34s on Azure, ~128s on AWS), so this timeout may simply be too "
"short rather than the insert having failed. Check the Zerto task before "
"assuming it did not land."
"If the protected site is Azure or AWS, tagged checkpoints are not supported."
)
+2 -1
View File
@@ -137,7 +137,8 @@ def find_from_rows(query: str, rows: list[dict[str, Any]]) -> FindResult:
outcome="none",
query=query,
message=(
f"VM {vm.vm_name} ({vm.vm_identifier}) has no VPG. Unprotected: refuse the change."
f"VM {vm.vm_name} ({vm.vm_identifier}) has no VPG. "
"Unprotected: refuse the change."
),
vm=vm,
)
+2 -3
View File
@@ -133,9 +133,8 @@ async def zerto_create_tagged_checkpoint(
the Zerto API accepts, so this is the only place that context can live.
Name format: ai:<agent> | <action> | vm=<vm> | change=<change_id> | <utc>
Works on Azure and AWS protected VPGs despite what the 9.0 API reference says,
but the tag appears late there (~34s Azure, ~128s AWS) and is stamped after the
request, so it may sit after a prompt mutation.
Docs: tagged checkpoints are not supported when the protected site is Azure or AWS;
run this against the vSphere protected ZVM.
"""
try:
result = await _find(query)
+2 -1
View File
@@ -127,6 +127,7 @@ def can_tag(status: int | None, substatus: int | None) -> tuple[bool, str | None
return False, f"VPG status is {status_name(status)}; not MeetingSLA"
if substatus in SYNCING:
return False, (
f"VPG is {substatus_name(substatus)}; checkpoints are not durable until sync ends"
f"VPG is {substatus_name(substatus)}; "
"checkpoints are not durable until sync ends"
)
return True, None