From 68c3e82dbe6f7e1c876c33e1f57ab6b297a437e6 Mon Sep 17 00:00:00 2001 From: Ava <170544612+Ava-91@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:41:48 +0330 Subject: [PATCH 1/3] feat(mirror): add preview-token-bound Mirror sidecar execution --- harmelune/sidecar.py | 97 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 3 deletions(-) diff --git a/harmelune/sidecar.py b/harmelune/sidecar.py index 745c6ff..6551c16 100644 --- a/harmelune/sidecar.py +++ b/harmelune/sidecar.py @@ -1,6 +1,6 @@ """Restricted JSONL bridge for the desktop UI. -Only explicitly enumerated scan, review, dry-run, and gated Safe/Reconcile +Only explicitly enumerated scan, review, dry-run, and gated Safe/Reconcile/Mirror execution commands are exposed. Both execution paths require an exact preview token and typed confirmation, validate backup locations against both libraries, and use the existing verified-backup transaction engine. Reconcile applies only @@ -152,6 +152,25 @@ def _reconcile_preview_token(plan_id: str, decisions: dict[str, ReconcileDecisio encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") return hashlib.sha256(encoded).hexdigest() + +def _mirror_preview_token(plan_id: str, master_library: str, backup_root: Path, preview: DryRunResult) -> str: + """Bind Mirror approval to direction, backup root, and every planned mutation.""" + payload = { + "plan_id": plan_id, + "master_library": master_library, + "backup_root": str(backup_root.resolve(strict=False)), + "operations": [ + {"kind": op.kind.value, "source": str(op.source) if op.source is not None else None, + "destination": str(op.destination)} + for op in preview.operations + ], + "skipped": preview.skipped, + "blocked": preview.blocked, + } + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + def _require_string(params: dict[str, object], name: str) -> str: value = params.get(name) if not isinstance(value, str) or not value.strip(): @@ -209,7 +228,7 @@ def _handle(request: object, emit_progress) -> tuple[dict[str, object], bool]: result: dict[str, object] = { "engine": "harmelune-python", "protocol_version": PROTOCOL_VERSION, - "capabilities": ["scan", "list_tracks", "preview_safe", "preview_reconcile", "execute_safe", "execute_reconcile", "shutdown"], + "capabilities": ["scan", "list_tracks", "preview_safe", "preview_reconcile", "execute_safe", "execute_reconcile", "preview_mirror", "execute_mirror", "shutdown"], "read_only": False, } return ({"version": PROTOCOL_VERSION, "id": request_id, "type": "result", "ok": True, "result": result}, False) @@ -411,13 +430,85 @@ def progress(side: str, visited: int) -> None: "result": {"execution": report.to_dict(), "mutations_enabled": True}, }, False) + if command == "preview_mirror": + plan_id = _require_string(params, "plan_id") + master_library = _require_string(params, "master_library") + backup_root_raw = _require_string(params, "backup_root") + stored = _PLANS.get(plan_id) + if stored is None: + raise ProtocolError("PLAN_NOT_FOUND", "This scan is no longer available. Scan both libraries again.") + bundle, rows = stored + if not _summary(bundle, rows)["safe_to_preview"]: + raise ProtocolError("SCAN_INCOMPLETE", "Mirror preview is blocked because one or both scans are incomplete or have errors.") + try: + direction = select_master(bundle.library_a, bundle.library_b, master_library) + backup_root = validate_backup_root(backup_root_raw, (bundle.library_a, bundle.library_b)) + preview = preview_workflow(bundle.plan, SyncMode.MIRROR, direction) + except StalePlanError as exc: + raise ProtocolError("STALE_PLAN", f"{exc} Rescan both libraries before previewing again.") from exc + except (OSError, ValueError) as exc: + raise ProtocolError("PREVIEW_BLOCKED", str(exc)) from exc + return ({ + "version": PROTOCOL_VERSION, "id": request_id, "type": "result", "ok": True, + "result": { + "mode": "mirror", "status": preview.status, + "preview_token": _mirror_preview_token(plan_id, master_library, backup_root, preview), + "operations": [ + {"kind": op.kind.value, "source": str(op.source) if op.source is not None else None, + "destination": str(op.destination)} + for op in preview.operations + ], + "skipped": preview.skipped, "blocked": preview.blocked, + "operation_count": len(preview.operations), + "deletion_count": sum(op.kind.value == "delete" for op in preview.operations), + "replacement_count": sum(op.kind.value == "replace" for op in preview.operations), + "mutations_enabled": False, + }, + }, False) + + if command == "execute_mirror": + plan_id = _require_string(params, "plan_id") + master_library = _require_string(params, "master_library") + backup_root_raw = _require_string(params, "backup_root") + supplied_token = _require_string(params, "preview_token") + if params.get("confirmation") != "MIRROR": + raise ProtocolError("CONFIRMATION_REQUIRED", 'Mirror execution requires exact confirmation text: MIRROR.') + stored = _PLANS.get(plan_id) + if stored is None: + raise ProtocolError("PLAN_NOT_FOUND", "This scan is no longer available. Scan both libraries again.") + bundle, rows = stored + if not _summary(bundle, rows)["safe_to_preview"]: + raise ProtocolError("SCAN_INCOMPLETE", "Mirror execution is blocked because one or both scans are incomplete or have errors.") + try: + direction = select_master(bundle.library_a, bundle.library_b, master_library) + backup_root = validate_backup_root(backup_root_raw, (bundle.library_a, bundle.library_b)) + preview = preview_workflow(bundle.plan, SyncMode.MIRROR, direction) + expected_token = _mirror_preview_token(plan_id, master_library, backup_root, preview) + if not hmac.compare_digest(supplied_token, expected_token): + raise ProtocolError("PREVIEW_MISMATCH", "The reviewed Mirror operations, direction, or backup location changed. Build a fresh preview and confirm again.") + if preview.status != "SUCCESS" or preview.blocked: + raise ProtocolError("PREVIEW_BLOCKED", "Mirror execution is blocked until all unresolved matches are reviewed and the preview has no blockers.") + if any(operation.kind.value not in {"copy", "replace", "delete"} for operation in preview.operations): + raise ProtocolError("UNSAFE_OPERATION", "Mirror contains an unsupported operation.") + report = execute_workflow(bundle.plan, SyncMode.MIRROR, direction, backup_root, confirmation="MIRROR") + except StalePlanError as exc: + raise ProtocolError("STALE_PLAN", f"{exc} Rescan both libraries before executing.") from exc + except ProtocolError: + raise + except (OSError, ValueError, RuntimeError) as exc: + raise ProtocolError("EXECUTION_BLOCKED", str(exc)) from exc + return ({ + "version": PROTOCOL_VERSION, "id": request_id, "type": "result", "ok": True, + "result": {"execution": report.to_dict(), "mutations_enabled": True}, + }, False) + if command == "shutdown": return ({ "version": PROTOCOL_VERSION, "id": request_id, "type": "result", "ok": True, "result": {"stopped": True}, }, True) - raise ProtocolError("UNKNOWN_COMMAND", "Supported commands are hello, ping, scan, list_tracks, preview_safe, preview_reconcile, execute_safe, execute_reconcile, and shutdown.") + raise ProtocolError("UNKNOWN_COMMAND", "Supported commands are hello, ping, scan, list_tracks, preview_safe, preview_reconcile, execute_safe, execute_reconcile, preview_mirror, execute_mirror, and shutdown.") def main() -> int: From d24f849915de16207210aeedd28dcefef5747f48 Mon Sep 17 00:00:00 2001 From: Ava <170544612+Ava-91@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:42:14 +0330 Subject: [PATCH 2/3] test(mirror): cover exact confirmation, preview integrity, deletion backups --- tests/test_sidecar.py | 95 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/tests/test_sidecar.py b/tests/test_sidecar.py index 2c155a8..e6445dd 100644 --- a/tests/test_sidecar.py +++ b/tests/test_sidecar.py @@ -277,6 +277,101 @@ def test_preview_reports_stale_plan_after_library_changes(tmp_path: Path, monkey raise AssertionError("A stale plan must not produce a preview.") + +def test_mirror_preview_lists_deletions_and_execution_requires_exact_token_confirmation(tmp_path: Path, monkeypatch): + from harmelune import sidecar + + a, b = tmp_path / "Library A", tmp_path / "Library B" + a.mkdir() + b.mkdir() + write_wav(a / "Keep from A.wav", 80) + write_wav(b / "Delete from B.wav", 83) + backup_root = tmp_path / "mirror-backups" + before_a = {p.relative_to(a).as_posix(): p.read_bytes() for p in a.rglob("*") if p.is_file()} + before_b = {p.relative_to(b).as_posix(): p.read_bytes() for p in b.rglob("*") if p.is_file()} + monkeypatch.setattr(sidecar, "_PLANS", {}) + scan, _ = sidecar._handle( + json.loads(request("scan", "scan", {"library_a": str(a), "library_b": str(b)})), + lambda _event: None, + ) + plan_id = scan["result"]["plan_id"] + preview, _ = sidecar._handle( + json.loads(request("preview-mirror", "preview_mirror", { + "plan_id": plan_id, "master_library": "library_a", "backup_root": str(backup_root), + })), + lambda _event: None, + ) + + assert preview["ok"] is True + assert preview["result"]["mode"] == "mirror" + assert preview["result"]["mutations_enabled"] is False + operations = preview["result"]["operations"] + assert {operation["kind"] for operation in operations} == {"copy", "delete"} + assert any(operation["kind"] == "delete" and operation["destination"] == str(b / "Delete from B.wav") for operation in operations) + token = preview["result"]["preview_token"] + + for confirmation, supplied_token, expected_code in [ + ("mirror", token, "CONFIRMATION_REQUIRED"), + ("MIRROR", "tampered-preview-token", "PREVIEW_MISMATCH"), + ]: + try: + sidecar._handle(json.loads(request("blocked-mirror", "execute_mirror", { + "plan_id": plan_id, "master_library": "library_a", "backup_root": str(backup_root), + "preview_token": supplied_token, "confirmation": confirmation, + })), lambda _event: None) + except sidecar.ProtocolError as exc: + assert exc.code == expected_code + else: + raise AssertionError(f"Mirror must reject invalid approval: {expected_code}") + assert {p.relative_to(a).as_posix(): p.read_bytes() for p in a.rglob("*") if p.is_file()} == before_a + assert {p.relative_to(b).as_posix(): p.read_bytes() for p in b.rglob("*") if p.is_file()} == before_b + assert not backup_root.exists() + + result, _ = sidecar._handle( + json.loads(request("apply-mirror", "execute_mirror", { + "plan_id": plan_id, "master_library": "library_a", "backup_root": str(backup_root), + "preview_token": token, "confirmation": "MIRROR", + })), + lambda _event: None, + ) + assert result["ok"] is True + report = result["result"]["execution"] + assert report["mode"] == "mirror" + assert report["final_status"] == "SUCCESS" + assert (b / "Keep from A.wav").read_bytes() == (a / "Keep from A.wav").read_bytes() + assert not (b / "Delete from B.wav").exists() + backup_dirs = list(backup_root.iterdir()) + assert len(backup_dirs) == 1 + assert (backup_dirs[0] / "Delete from B.wav").read_bytes() == before_b["Delete from B.wav"] + assert {p.relative_to(a).as_posix(): p.read_bytes() for p in a.rglob("*") if p.is_file()} == before_a + + +def test_mirror_preview_rejects_backup_root_inside_either_library(tmp_path: Path, monkeypatch): + from harmelune import sidecar + + a, b = tmp_path / "Library A", tmp_path / "Library B" + a.mkdir() + b.mkdir() + write_wav(a / "Only A.wav", 80) + write_wav(b / "Only B.wav", 83) + monkeypatch.setattr(sidecar, "_PLANS", {}) + scan, _ = sidecar._handle( + json.loads(request("scan", "scan", {"library_a": str(a), "library_b": str(b)})), + lambda _event: None, + ) + plan_id = scan["result"]["plan_id"] + for unsafe_root in (a / "backup", b / "backup"): + try: + sidecar._handle(json.loads(request("unsafe-mirror", "preview_mirror", { + "plan_id": plan_id, "master_library": "library_a", "backup_root": str(unsafe_root), + })), lambda _event: None) + except sidecar.ProtocolError as exc: + assert exc.code == "PREVIEW_BLOCKED" + else: + raise AssertionError("Mirror must reject backup roots inside either library.") + + + def test_safe_execution_requires_exact_confirmation_and_verified_preview(tmp_path: Path, monkeypatch): from harmelune import sidecar From 9b0669b988f40415eb2b9cf676543a27ea9d2c2f Mon Sep 17 00:00:00 2001 From: Ava <170544612+Ava-91@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:42:57 +0330 Subject: [PATCH 3/3] feat(desktop): add full-list Mirror preview and destructive approval gate --- desktop/src/App.tsx | 185 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 184 insertions(+), 1 deletion(-) diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx index 012cf7c..11de242 100644 --- a/desktop/src/App.tsx +++ b/desktop/src/App.tsx @@ -7,6 +7,7 @@ const sections = [ { id: "library", label: "Library", mark: "▤", hint: "Select and scan folders" }, { id: "compare", label: "Compare", mark: "⇄", hint: "Review library differences" }, { id: "review", label: "Review & preview", mark: "◫", hint: "Resolve uncertain matches" }, + { id: "mirror", label: "Mirror", mark: "⇌", hint: "Review exact changes before mirroring" }, { id: "backups", label: "Backups & recovery", mark: "↺", hint: "Verified backups and recovery" }, { id: "history", label: "Sync history", mark: "◷", hint: "Past operation reports" }, { id: "health", label: "Library health", mark: "⌁", hint: "Metadata and file health" }, @@ -64,6 +65,13 @@ function App() { const [reconcileConfirmation, setReconcileConfirmation] = useState(""); const [reconcileExecuting, setReconcileExecuting] = useState(false); const [reconcileReport, setReconcileReport] = useState | null>(null); + const [mirrorMasterLibrary, setMirrorMasterLibrary] = useState("library_a"); + const [mirrorBackupRoot, setMirrorBackupRoot] = useState(""); + const [mirrorPreview, setMirrorPreview] = useState(null); + const [mirrorPreviewing, setMirrorPreviewing] = useState(false); + const [mirrorConfirmation, setMirrorConfirmation] = useState(""); + const [mirrorExecuting, setMirrorExecuting] = useState(false); + const [mirrorReport, setMirrorReport] = useState | null>(null); const [reviewRows, setReviewRows] = useState([]); const [reviewPage, setReviewPage] = useState(0); const [reviewTotal, setReviewTotal] = useState(0); @@ -156,6 +164,9 @@ function App() { setReconcileDecisions({}); setReconcileConfirmation(""); setReconcileReport(null); + setMirrorPreview(null); + setMirrorConfirmation(""); + setMirrorReport(null); setSafeConfirmation(""); setExecutionReport(null); setStalePlan(false); @@ -189,6 +200,9 @@ function App() { setReconcileDecisions({}); setReconcileConfirmation(""); setReconcileReport(null); + setMirrorPreview(null); + setMirrorConfirmation(""); + setMirrorReport(null); setSafeConfirmation(""); setExecutionReport(null); setStalePlan(false); @@ -357,6 +371,81 @@ function App() { setReconcileExecuting(false); } }; + const buildMirrorPreview = async () => { + if (!planId || !summary) { + setError("Scan both libraries before building a Mirror preview."); + return; + } + if (!summary.safe_to_preview) { + setError("Mirror is blocked because one or both scans have errors or are incomplete."); + return; + } + if (!mirrorBackupRoot.trim()) { + setError("Choose a separate backup location outside both libraries first."); + return; + } + setError(""); + setStalePlan(false); + setMirrorPreviewing(true); + setMirrorPreview(null); + setMirrorConfirmation(""); + setMirrorReport(null); + try { + const response = await engineRequest("preview_mirror", { + plan_id: planId, + master_library: mirrorMasterLibrary, + backup_root: mirrorBackupRoot.trim(), + }); + if (!response.ok) { + setStalePlan(["STALE_PLAN", "PLAN_NOT_FOUND"].includes(response.error?.code ?? "")); + throw new Error(response.error?.message ?? "Could not build the Mirror preview."); + } + setMirrorPreview(response.result as unknown as SafePreview); + } catch (reason) { + setError(reason instanceof Error ? reason.message : String(reason)); + } finally { + setMirrorPreviewing(false); + } + }; + + const executeMirror = async () => { + if (!planId || !summary || !mirrorPreview?.preview_token || !mirrorBackupRoot.trim()) { + setError("Build a fresh Mirror preview and choose a separate backup location first."); + return; + } + if (mirrorConfirmation !== "MIRROR") { + setError('Type MIRROR exactly to authorize the destructive Mirror operation.'); + return; + } + if (mirrorPreview.status !== "SUCCESS" || mirrorPreview.blocked.length || + mirrorPreview.operations.some((operation) => !["copy", "replace", "delete"].includes(operation.kind))) { + setError("Mirror execution is blocked until the preview is successful, unblocked, and contains only supported operations."); + return; + } + setError(""); + setMirrorExecuting(true); + try { + const response = await engineRequest("execute_mirror", { + plan_id: planId, + master_library: mirrorMasterLibrary, + backup_root: mirrorBackupRoot.trim(), + preview_token: mirrorPreview.preview_token, + confirmation: mirrorConfirmation, + }); + if (!response.ok) { + setStalePlan(["STALE_PLAN", "PLAN_NOT_FOUND", "PREVIEW_MISMATCH"].includes(response.error?.code ?? "")); + throw new Error(response.error?.message ?? "Mirror execution was blocked."); + } + setMirrorReport((response.result as Record).execution); + setMirrorPreview(null); + setMirrorConfirmation(""); + } catch (reason) { + setError(reason instanceof Error ? reason.message : String(reason)); + } finally { + setMirrorExecuting(false); + } + }; + const section = sections.find((item) => item.id === active) ?? sections[0]; const hasScan = Boolean(planId && summary); const selectedRows = rows; @@ -600,7 +689,101 @@ function App() { )} - {active !== "library" && active !== "compare" && active !== "review" && ( + {active === "mirror" && ( + <> +
+

Mirror — destructive synchronization

Make the destination match the selected master. Every copy, replacement, and deletion must be visible before approval.

+ EXPLICIT DELETION GATE +
+
+ + +
+
+ + { + setMirrorBackupRoot(event.target.value); + setMirrorPreview(null); + setMirrorConfirmation(""); + setMirrorReport(null); + }} placeholder="Choose a separate backup drive/folder…" /> + Mirror cannot execute with an incomplete scan, stale plan, changed backup location, or changed preview. + +
+ {!hasScan &&
Scan both libraries before planning Mirror.
} + {summary && !summary.safe_to_preview &&
Mirror blocked: both scans must finish without errors before a preview can be approved.
} + {mirrorPreview && ( +
+
+
All operations{mirrorPreview.operation_count}full list below
+
Deletions{mirrorPreview.operations.filter((operation) => operation.kind === "delete").length}destination files removed
+
Replacements{mirrorPreview.operations.filter((operation) => operation.kind === "replace").length}destination files replaced
+
Preview status{mirrorPreview.status}{mirrorPreview.blocked.length} blocker(s)
+
+
+ Review every planned mutation +

Nothing has changed yet. The complete operation list is shown here, not truncated. Read the delete and replacement destinations carefully.

+
+

Exact planned operations ({mirrorPreview.operations.length})

+ {mirrorPreview.operations.length ? ( +
    {mirrorPreview.operations.map((operation, index) => ( +
  • + {operation.kind.toUpperCase()} + {operation.source ?? "—"}→ {operation.destination} +
  • + ))}
+ ) :
No operations are planned; Mirror has nothing to change.
} + {mirrorPreview.blocked.length > 0 &&
Blocked items — execution unavailable
    {mirrorPreview.blocked.map((item, index) =>
  • {item}
  • )}
} + {mirrorPreview.operations.some((operation) => operation.kind === "delete") && ( +
+ Exact deletion list ({mirrorPreview.operations.filter((operation) => operation.kind === "delete").length}) +
    {mirrorPreview.operations.filter((operation) => operation.kind === "delete").map((operation, index) =>
  • {operation.destination}
  • )}
+
+ )} + {mirrorPreview.operations.some((operation) => operation.kind === "replace") && ( +
!
Exact replacement list ({mirrorPreview.operations.filter((operation) => operation.kind === "replace").length})
    {mirrorPreview.operations.filter((operation) => operation.kind === "replace").map((operation, index) =>
  • {operation.destination} ← {operation.source}
  • )}
+ )} +
+

Mirror execution — final destructive gate

+

Before any destination changes, Harmelune creates and verifies a backup of the destination library. The Python transaction engine rechecks freshness and performs the approved plan.

+ + setMirrorConfirmation(event.target.value)} placeholder="MIRROR" autoComplete="off" /> + + Changing the direction or backup folder invalidates this preview. Any stale or altered plan requires a new scan and approval. +
+
+ )} + {mirrorReport && ( +
+

Mirror execution report

+
+
Final status{String(mirrorReport.final_status)}
+
Succeeded{Number(mirrorReport.succeeded ?? 0)}
+
Skipped{Number(mirrorReport.skipped ?? 0)}
+
Failed{Number(mirrorReport.failed ?? 0)}
+
+

Verified backup(s): {(mirrorReport.backups ?? []).join(", ") || "No backup was required."}

+
Full execution report
{JSON.stringify(mirrorReport, null, 2)}
+
+ )} + {stalePlan &&
Plan is stale

One or both libraries changed after scanning. Rescan before building or executing another preview.

} + {error &&
{error}
} + + )} + + {active !== "library" && active !== "compare" && active !== "review" && active !== "mirror" && (

{section.label} is waiting for its safety workflow

{section.hint}. This section remains unavailable until review, freshness validation, verified backups, and recovery behavior are connected to the Python engine.

)}