fix(flr): partition-rooted FLR paths + agent/intent in checkpoint names #2

Merged
claude merged 2 commits from fix/flr-partition-path into main 2026-09-21 13:42:36 -04:00
Contributor

Two fixes found while recording an end-to-end demo of the rewind loop against the lab ZVM (10.x, VPG jp-ubuntu). Both verified live, not just unit-tested.

1. FLR path resolution (94025b8)

zerto_recover_file passed the guest absolute path straight to POST /v1/flrs/{session}/download, which the ZVM rejects:

HTTP 400 {"Message":"Invalid path: check location exists or correct path syntax."}

FLR browse/download is rooted at partitions, not the guest's /. /home/justin/app-config.yaml is Volume2-Ext4/home/justin/app-config.yaml. server.py already called browse_flr() but threw the result away, so nothing ever resolved it — meaning file recovery was broken for any caller.

resolve_flr_path() / browsable_partitions() now handle it:

  • browse path="" returns {MainPathItem, PathItems}, not a bare list
  • skip partitions with IsBrowsable: false — a Linux guest reports Volume1-Unknown as "Cannot restore. Partition type Unknown is not supported." Do not assume the first partition.
  • browse returns child paths percent-encoded (...%2fhome%2fjustin%2f...); download wants them decoded with plain slashes
  • on a miss, raise naming what was searched, since "not replicated into that checkpoint yet" is the likely cause and is actionable

zerto_recover_file now returns the resolved flr_path.

2. Checkpoint names carry agent + intent (fe7b220)

Zerto's tagged-checkpoint insert takes exactly one field — the 10.x swagger model VpgInsertTagCheckpointDataApi has a single property, checkpointName, and the 9.0 API reference lists CheckpointName as the only request value. There is no description field, so who the agent is and what it is about to do have to live inside the name.

before:  ai:claude:chg-412:20260921T170829Z
after:   ai:claude | edit /home/justin/app-config.yaml | vm=jp-ubuntu | change=chg-412 | 20260921T170829Z

zerto_create_tagged_checkpoint and zerto_guard_before_mutate take a new action argument; the VM name is filled from the find result. An operator reading the journal in the Zerto UI can now see which agent inserted a checkpoint and why, without the agent transcript.

Field text is sanitised so the name stays one readable line: control chars and whitespace runs collapse, ; becomes , (Zerto appends "; Used for File Level Restore" to its own tags) and | becomes / (our field separator). Capped at TAG_MAX_LEN (250).

Measured against the live ZVM while picking the format

  • Checkpoint names of at least 400 chars are accepted; spaces, slashes, parentheses, = and | all survive the round trip. The 250 cap is for UI readability, not an API limit.
  • Tagged checkpoint inserts fired back to back at one VPG are silently dropped. The POST returns 200 and queues a task, but only the first checkpoint ever appears. tag_vpgs already inserts-then-waits per VPG so it is correct; added a comment so nobody turns that loop into an asyncio.gather(). This initially looked like a length limit and then a charset limit before it became clear only the first insert of each burst landed.

Verification

Full loop through real MCP stdio against ZVM 10.x:

  • guard inserted cp 1368 on VPG jp-ubuntu, name read back byte-identical from the journal
  • file mutated on the guest after the checkpoint
  • zerto_recover_file without confirmed correctly refused (needs_confirm: true)
  • FLR from that pre-mutation checkpoint returned the 158-byte original; md5 of the recovered file matches the pre-mutation md5 7c019cb8e27d93bcc2afc03b2f6c5ce4
  • FLR mounts in ~3s and sessions are torn down: the ZVM shows no leaked sessions across ~8 runs

pytest: 22 passed (5 new). ruff check clean on all changed files. Docs updated: SKILL.md, README.md, CONTEXT.md.

Pre-existing ruff findings in client.py (PIE810, E501) and formatting in protection.py are untouched — out of scope for this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn

Two fixes found while recording an end-to-end demo of the rewind loop against the lab ZVM (10.x, VPG `jp-ubuntu`). Both verified live, not just unit-tested. ## 1. FLR path resolution (`94025b8`) `zerto_recover_file` passed the guest absolute path straight to `POST /v1/flrs/{session}/download`, which the ZVM rejects: ``` HTTP 400 {"Message":"Invalid path: check location exists or correct path syntax."} ``` FLR browse/download is rooted at **partitions**, not the guest's `/`. `/home/justin/app-config.yaml` is `Volume2-Ext4/home/justin/app-config.yaml`. `server.py` already called `browse_flr()` but threw the result away, so nothing ever resolved it — meaning file recovery was broken for any caller. `resolve_flr_path()` / `browsable_partitions()` now handle it: - browse `path=""` returns `{MainPathItem, PathItems}`, not a bare list - skip partitions with `IsBrowsable: false` — a Linux guest reports `Volume1-Unknown` as *"Cannot restore. Partition type Unknown is not supported."* Do not assume the first partition. - browse returns child paths percent-encoded (`...%2fhome%2fjustin%2f...`); download wants them decoded with plain slashes - on a miss, raise naming what was searched, since "not replicated into that checkpoint yet" is the likely cause and is actionable `zerto_recover_file` now returns the resolved `flr_path`. ## 2. Checkpoint names carry agent + intent (`fe7b220`) Zerto's tagged-checkpoint insert takes exactly one field — the 10.x swagger model `VpgInsertTagCheckpointDataApi` has a single property, `checkpointName`, and the 9.0 API reference lists `CheckpointName` as the only request value. There is no description field, so who the agent is and what it is about to do have to live inside the name. ``` before: ai:claude:chg-412:20260921T170829Z after: ai:claude | edit /home/justin/app-config.yaml | vm=jp-ubuntu | change=chg-412 | 20260921T170829Z ``` `zerto_create_tagged_checkpoint` and `zerto_guard_before_mutate` take a new `action` argument; the VM name is filled from the find result. An operator reading the journal in the Zerto UI can now see which agent inserted a checkpoint and why, without the agent transcript. Field text is sanitised so the name stays one readable line: control chars and whitespace runs collapse, `;` becomes `,` (Zerto appends `"; Used for File Level Restore"` to its own tags) and `|` becomes `/` (our field separator). Capped at `TAG_MAX_LEN` (250). ## Measured against the live ZVM while picking the format - Checkpoint names of at least **400 chars** are accepted; spaces, slashes, parentheses, `=` and `|` all survive the round trip. The 250 cap is for UI readability, not an API limit. - **Tagged checkpoint inserts fired back to back at one VPG are silently dropped.** The POST returns 200 and queues a task, but only the first checkpoint ever appears. `tag_vpgs` already inserts-then-waits per VPG so it is correct; added a comment so nobody turns that loop into an `asyncio.gather()`. This initially looked like a length limit and then a charset limit before it became clear only the *first* insert of each burst landed. ## Verification Full loop through real MCP stdio against ZVM 10.x: - guard inserted cp 1368 on VPG `jp-ubuntu`, name read back byte-identical from the journal - file mutated on the guest *after* the checkpoint - `zerto_recover_file` without `confirmed` correctly refused (`needs_confirm: true`) - FLR from that pre-mutation checkpoint returned the 158-byte original; md5 of the recovered file matches the pre-mutation md5 `7c019cb8e27d93bcc2afc03b2f6c5ce4` - FLR mounts in ~3s and sessions are torn down: the ZVM shows no leaked sessions across ~8 runs `pytest`: 22 passed (5 new). `ruff check` clean on all changed files. Docs updated: `SKILL.md`, `README.md`, `CONTEXT.md`. Pre-existing `ruff` findings in `client.py` (PIE810, E501) and formatting in `protection.py` are untouched — out of scope for this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
claude added 2 commits 2026-09-21 13:36:04 -04:00
zerto_recover_file passed the guest absolute path straight to
POST /v1/flrs/{session}/download, which the ZVM rejects:

  HTTP 400 {"Message":"Invalid path: check location exists or
  correct path syntax."}

FLR browse/download is rooted at partitions, not the guest's /.
/home/justin/app-config.yaml is Volume2-Ext4/home/justin/app-config.yaml.
server.py already called browse_flr() but discarded the result, so
nothing ever resolved the path.

Add resolve_flr_path() and browsable_partitions() to recover.py:

- browse path "" returns {MainPathItem, PathItems}, not a bare list
- skip partitions with IsBrowsable false. A Linux guest reports
  Volume1-Unknown as "Cannot restore. Partition type Unknown is not
  supported." Do not assume the first partition is the right one.
- browse returns child paths percent-encoded
  (Volume2-Ext4%2fhome%2fjustin%2fapp-config.yaml); download wants
  them decoded with plain slashes
- raise a ZertoError naming what was searched when the file is
  absent, since "not replicated into that checkpoint yet" is the
  likely cause and is actionable

zerto_recover_file now returns the resolved flr_path.

Verified end to end against ZVM 10.x: guard tagged cp 1075 on VPG
jp-ubuntu, FLR mounted in ~3s, 158 bytes recovered from the
pre-mutation checkpoint with matching content.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
Zerto's tagged checkpoint insert takes exactly one field. The 10.x
swagger model VpgInsertTagCheckpointDataApi has a single property,
checkpointName, and the 9.0 API reference lists CheckpointName as the
only request value. There is no description field, so who the agent is
and what it is about to do have to live inside the name.

Old name:
  ai:claude:chg-412:20260921T170829Z

New name:
  ai:claude | edit /home/justin/app-config.yaml | vm=jp-ubuntu |
  change=chg-412 | 20260921T170829Z

zerto_create_tagged_checkpoint and zerto_guard_before_mutate take a new
action argument: free text saying what the agent is about to do. The VM
name is filled in from the find result. An operator reading the journal
in the Zerto UI can now see which agent inserted a checkpoint and why,
without the agent transcript.

Field text is sanitised so the name stays one readable line: control
characters and runs of whitespace collapse to single spaces, ';' becomes
',' because Zerto appends "; Used for File Level Restore" to its own
tags, and '|' becomes '/' because ' | ' is our field separator. Capped
at TAG_MAX_LEN (250).

Measured against ZVM 10.x while picking the format:

- names of at least 400 chars are accepted, and spaces, slashes,
  parentheses, '=' and '|' all survive the round trip
- tagged checkpoint inserts fired back to back at one VPG are silently
  dropped. The POST returns 200 and queues a task, but only the first
  checkpoint appears. tag_vpgs already inserts then waits per VPG, so
  it is correct; added a comment so nobody turns that loop into an
  asyncio.gather().

Verified end to end: guard inserted cp 1197 on VPG jp-ubuntu, the name
read back byte-identical from the journal, and FLR from that checkpoint
returned the 158 byte pre-mutation file.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
claude merged commit 108e919dcb into main 2026-09-21 13:42:36 -04:00
claude deleted branch fix/flr-partition-path 2026-09-21 13:42:36 -04:00
Sign in to join this conversation.