Skip to content

fix: preserve private Windows ACLs through backup, restore, and quarantine journal - #960

Merged
ScriptedAlchemy merged 10 commits into
codex/tracedecay-total-redesign-plan-reopenedfrom
cursor/fix-920-windows-acl-backup-dfec
Sep 7, 2026
Merged

fix: preserve private Windows ACLs through backup, restore, and quarantine journal#960
ScriptedAlchemy merged 10 commits into
codex/tracedecay-total-redesign-plan-reopenedfrom
cursor/fix-920-windows-acl-backup-dfec

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Summary

  • Backup and restore now create every plain-copied artifact (above all profile-identity.json), the backup manifest, and the rehearsal marker through tracedecay_private_fs::create_private_file, so the copies carry the exact owner-private mode/DACL the strict readers admit instead of what the destination directory would let fs::copy inherit.
  • The remote-restore quarantine fence is now published through DatabaseAuthority::publish_record_atomically, the private record authority paired with the read_record_strict reader that was refusing it.
  • Fixtures publish the identity record through the daemon's record authority so negative-schema and verified-backup tests reach the real contract boundary; fault-rehearsal tests report the concrete earlier refusal when the injected boundary or its marker never appears.

Fixes #920. Targets codex/tracedecay-total-redesign-plan-reopened (#707); #707 stays draft.

Motivation

In the #707 Windows baseline (run 34039753429) 16 rows failed with private Windows DACL validation failed: SecuritySnapshot { owner_is_current_user: false, dacl_is_protected: false, ace_count: 3, ... } on either the copied profile-identity.json or the freshly written sessions.remote-restore-quarantine.json. Three distinct defects, one root class: a file was created by a plain OS primitive (fs::copy, OpenOptions::create) and then read by the strict private authority.

Failing rows First refusal Derivatives
profile_backup::tests::* (3), profile_backup_rehearsal_test::* (7), verified_profile_backup::* (2) fs::copy of profile-identity.json into the backup root / restore staging (snapshot_artifact, copy_verified_file); verified_profile_backup also seeded the source identity with fs::write marker NotFound, "injected rehearsal publication fault" assertion misses
quarantine::tests::* (3) PrivateStoreIo::write_file_atomically_durable creates the fence with the inherited ACL; read_record_strict refuses it
profile_identity::tests::unknown_fields_and_schema_versions_fail_closed fixture fs::write refused at admission before parsing

Changes

  • crates/tracedecay-maintenance/src/profile_backup.rs
    • New copy_private_artifact: opens the source read-only, creates the destination via tracedecay_private_fs::create_private_file, streams bytes, sync_alls, and removes the partial destination on failure. Used by copy_verified_file (restore) and snapshot::snapshot_artifact's plain-copy branch (backup). Bytes and durable identity are unchanged; the source is never re-permissioned.
    • write_new_synced (manifest + rehearsal marker) creates through the same authority (still create_new semantics, 0600 on Unix).
    • restrict_private_directory gains its Windows analogue via make_private_directory on the just-created restore staging directory. This is the same "make our own new directory private" step as the existing chmod 0700, not a repair of foreign material.
  • crates/tracedecay-store-runtime/.../publication/quarantine.rs: write publishes through DatabaseAuthority::publish_record_atomically with the record name shared with the reader; clears this attempt's own leftover .staging first so an interrupted write cannot block the next (the previous truncating write tolerated that too). New test install_replaces_a_stale_staging_record_left_by_an_interrupted_write.
  • crates/tracedecay-runtime-core/src/storage/profile_identity.rs (tests): write_identity publishes via publish_record_atomically.
  • crates/tracedecay/tests/product_surface_suite/verified_profile_backup.rs: seed_profile publishes the identity via publish_record_atomically.
  • crates/tracedecay/tests/product_surface_suite/profile_backup_rehearsal_test.rs: rehearse_until_injected_fault / interrupt_published_rehearsal helpers assert the named boundary was reached and include the actual error when the marker is missing.
  • crates/tracedecay-maintenance/src/profile_backup/tests.rs: asserts the backup-root and restored identity records are admitted by read_existing_profile_identity_record, not merely byte-equal.

No private-fs check was weakened; wrong-owner / unprotected-DACL files are still refused (permissive_existing_file_is_rejected_without_acl_rewrite, rehearsal_rejects_identity_tampered_backup_material, and insecure_or_symlinked_identity_fails_closed are unchanged).

Test plan

  • Linux: cargo test -p tracedecay-maintenance --lib profile_backup (8/8), cargo test -p tracedecay-store-runtime --lib remote_recovery (12/12 incl. new test), cargo test -p tracedecay-runtime-core --lib profile_identity (4/4), cargo test -p tracedecay --test product_surface_suite -- profile_backup (10/10)
  • cargo clippy --all-targets --locked -- -D warnings on tracedecay-maintenance, tracedecay-store-runtime, tracedecay-runtime-core, and the product_surface_suite test target: clean
  • cargo fmt --all -- --check: clean
  • cargo check --target x86_64-pc-windows-gnu --tests for the three library crates: clean (the cfg(windows) branch compiles against the make_private_directory re-export). The tracedecay crate itself cannot be cross-checked here (ort has no windows-gnu prebuilt); its edits are platform-neutral.
  • Windows CI lanes on this PR are the real verification of the 16 rows; no Windows runner is available in this environment.

Checklist

Open in Web Open in Cursor 

cursoragent and others added 3 commits September 6, 2026 20:43
Plain `fs::copy` let every plain-copied backup artifact, above all
`profile-identity.json`, inherit the destination directory's ACL on
Windows (three inherited ACEs, default token owner, unprotected DACL).
The strict private reader then refused the copied record in both the
backup root (`load_verified_backup`) and the restored profile, so every
rehearsal failed at ACL admission before reaching its named fault
boundary. The marker `NotFound` and "injected fault" assertion failures
were derivatives of that first refusal.

Copy plain artifacts through `tracedecay_private_fs::create_private_file`
on both the backup and restore sides, create the manifest and rehearsal
marker the same way, and give the restore staging directory its Windows
private analogue of the existing 0700 mode. Bytes are preserved exactly
and the source is never re-permissioned.

Fixtures: seed the verified-backup profile identity through the daemon's
record authority so the test reaches the backup contract on Windows, and
make the fault-rehearsal tests report the concrete earlier refusal when
the injected boundary or its publication marker never appears. Assert the
copied identity is admitted by the strict reader, not merely byte-equal.

Refs #920

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
The quarantine journal was written with `PrivateStoreIo`
(plain create on Windows, inheriting the parent's ACL) but read back
through `DatabaseAuthority::read_record_strict`, which admits only the
exact protected current-user DACL. Every restart-recovery test therefore
failed at "secure remote restore quarantine fence before read".

Publish the fence through `DatabaseAuthority::publish_record_atomically`,
the same private record authority the reader pairs with, clearing this
attempt's own stale staging record first so an interrupted write never
blocks the next one.

Refs #920

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
The negative schema tests wrote the record with `fs::write`, so on
Windows the reader refused it at private-file admission and the
"unknown field" assertion never exercised parsing. Publish the fixture
through the same record authority the daemon mints with.

Refs #920

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Sep 6, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: e310f7e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

This branch no longer merges cleanly with codex/tracedecay-total-redesign-plan-reopened (tip 5808a75). Conflicting files when merging the base in: crates/tracedecay-maintenance/src/profile_backup.rs crates/tracedecay-maintenance/src/profile_backup/snapshot.rs crates/tracedecay/tests/product_surface_suite/verified_profile_backup.rs. Needs a rebase or base merge by the author; I left the branch untouched. The peer-landed work most likely behind the conflict: 5808a75 (registrar keyed by store authority), 2cc9808 (capability digest), b098810 (git argument paths on Windows), 6504d0c (private DACL), d265aa4 (host fixture paths).

# Conflicts:
#	crates/tracedecay-maintenance/src/profile_backup.rs
#	crates/tracedecay-maintenance/src/profile_backup/snapshot.rs
#	crates/tracedecay/tests/product_surface_suite/verified_profile_backup.rs

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Architecture review — HOLD for ownership proof, conflicts and Windows evidence

Reviewed 07a782cfcb4768758929a3efc0583e3960552498; source review only. GitHub reports conflicts; main CI is queued and hotpath coverage failed, so this is not verified merge-state evidence.

Creating backup copies, manifests and restore fences through the private-file authority is correct. The source must remain untouched, destination create-new semantics must remain strict, and the real private reader—not byte equality alone—is the acceptance boundary.

P1: remote_recovery/publication/quarantine.rs::write unconditionally removes the shared fence.with_extension("staging") before publishing. Its comment calls that file 'this attempt's own', but the path does not establish ownership. Make the existing exclusive restore/terminal-vacancy fence explicit at this operation and prove no live publisher can own that staging file, or use an attempt-qualified staging record whose cleanup verifies ownership. Add concurrent/interrupted publisher and foreign-staging tests. Do not unlink another live attempt's file to make create-new succeed.

P1 acceptance: native Windows backup -> restore -> strict identity read; wrong-owner/unprotected-DACL rejection without repair; partial copy/sync failure with no published manifest; resumed restore does not overwrite a foreign destination; and quarantine recovery preserves the exact terminal vacancy. An x86_64-pc-windows-gnu compile check is not DACL verification.

Resolve the current #707 conflicts alongside #946's handle-lifetime fixes without globally closing owners or broadening permissions. Classify the hotpath failure on the final tree rather than disabling coverage. No #707 draft or release change.

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Resolved against the current redesign base and pushed as e5d91583d (0 commits behind at verification).

Conflict decisions:

  • Kept central 6504d0ca2 / current copy_private_file and the central verified-backup fixture hardening; dropped this branch's duplicate stream-copy helper and duplicate fixture publication.
  • Retained the real additions: private creation for backup manifest/rehearsal markers and the owned staging directory; strict-reader assertions for backed-up/restored identity records; admitted negative-schema identity fixtures; DatabaseAuthority quarantine publication with stale-staging recovery; and fault tests that prove their named boundary was reached.

Verification on the integrated tree: 8 maintenance backup tests, 12 remote-recovery tests, 4 profile-identity tests, and 10 product backup journeys passed; scoped clippy reported 0 warnings; actionlint and rustfmt passed; Windows GNU cross-check completed with 0 errors (13 pre-existing cfg-target warnings outside this diff). Issue #920 remains open pending real Windows CI acceptance.

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Final base resync: pushed ffc2e8277, now 0 commits behind b1afc0bbe. The additional base range only reorganized/added daemon dirty-worktree tests and did not touch the five PR files; actionlint, rustfmt, diff checks, and ancestry checks passed again. The scoped behavioral and clippy evidence in the prior resolution comment remains the applicable verification.

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Final ancestry confirmation: pushed fb595791c; both this head and PR #953 are now 0 commits behind base 7b67254c2. This last sync added only the base agent-host/automation refactor and retained the same five-file PR delta; final actionlint, rustfmt, diff, and ancestry checks passed.

@ScriptedAlchemy
ScriptedAlchemy marked this pull request as ready for review September 7, 2026 03:34
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-07T03:35:45.949511Z e310f7e Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ScriptedAlchemy
ScriptedAlchemy merged commit 20e693d into codex/tracedecay-total-redesign-plan-reopened Sep 7, 2026
4 checks passed

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System-level re-review at e310f7e27 — narrowed after tracing the restore owner

The current remaining diff is substantially smaller than the original proposal: private creation for manifest/rehearsal records, Windows staging-directory privacy, private quarantine publication, and the associated tests. Do not reintroduce duplicate copy helpers already absorbed into the target.

I traced RemoteRecoveryPublicationContextV1::publish_restore: it validates the registered binding, reserves the project replacement, quiesces the project, retires the exact Graph/Store pair, obtains the terminal vacancy, then writes the quarantine fence before awaiting destructive maintenance. This matters: the journal records an irreversible owner-retirement boundary; it is not just a backup status file. Keep that ordering, and keep post-swap recovery bound to the exact published/rollback file identities. A later cancellation cannot restore the already-closed owner by inference.

Correction to the earlier staging comment: the fixed staging filename alone is not proof of two legitimate concurrent publishers. There is an existing per-project replacement/recovery ownership protocol. I am not requesting another global mutex or presenting a reproduced concurrent overwrite here.

Remaining P2 ownership/acceptance gap: quarantine::write still deletes whatever occupies the fixed .staging path before private creation, and its new test only seeds arbitrary bytes there and expects deletion. Make the existing exclusion and ownership premise explicit at this boundary. Exercise the actual replacement/recovery path with an interrupted publisher and competing admission, proving only the exact restore owner can clear its scratch record and that a foreign/newer authoritative fence is untouched. If that exclusion cannot cover every writer of this path, use attempt-owned scratch publication rather than deletion by filename. Do not broaden cleanup or weaken private-file validation.

The ACL direction is appropriate: create new artifacts with the policy the strict reader requires, rather than repairing arbitrary input on read. For staging-directory permission changes, retain the premise that this is the newly created restore directory, not an already-existing foreign directory. Native Windows verification must reopen the produced identity/fence through the real strict reader and exercise interruption on both sides of publication; byte equality alone is insufficient.

No additional confirmed production defect was established in the inspected private-creation changes. Exact-head CI is pending and hotpath runs queued; this review is source analysis, not Windows execution or a release approval.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Performance Comparison codex/tracedecay-total-redesign-plan-reopenedcursor/fix-920-windows-acl-backup-dfec

Total Elapsed Time: 3.89s → 3.93s (+0.9%)
CPU Baseline: 77.96µs → 76.68µs (-1.6%)
Benchmark ID: index-bench-timing

timing - Execution duration of functions.

+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| Function                                 | Calls                      | Avg                            | P95                            | Total                           | % Total                      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| tracedecay-index-bench                   | 1 → 1 (+0.0%)              | 3.89s → 3.93s (+1.0%)          | 3.89s → 3.93s (+1.0%)          | 3.89s → 3.93s (+1.0%)           | 100.00% → 100.00% (+0.0%)    |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| query.artifact.append_pages              | 13 → 13 (+0.0%)            | 146.55ms → 149.18ms (+1.8%)    | 196.35ms → 202.11ms (+2.9%)    | 1.91s → 1.94s (+1.6%)           | 48.94% → 49.35% (+0.8%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| query.artifact.batch.sqlite              | 13 → 13 (+0.0%)            | 100.57ms → 103.30ms (+2.7%)    | 134.48ms → 141.16ms (+5.0%)    | 1.31s → 1.34s (+2.3%)           | 33.58% → 34.18% (+1.8%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| code_index.workers.install               | 83 → 83 (+0.0%)            | 11.71ms → 11.57ms (-1.2%)      | 40.83ms → 40.44ms (-1.0%)      | 971.97ms → 960.52ms (-1.2%)     | 24.97% → 24.44% (-2.1%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| query.artifact.batch.postings            | 13 → 13 (+0.0%)            | 73.30ms → 72.74ms (-0.8%)      | 99.88ms → 98.63ms (-1.3%)      | 952.86ms → 945.61ms (-0.8%)     | 24.48% → 24.06% (-1.7%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| domain.canonical.sha256                  | 114526 → 114526 (+0.0%)    | 6.99µs → 6.92µs (-1.0%)        | 11.71µs → 11.75µs (+0.3%)      | 801.05ms → 792.54ms (-1.1%)     | 20.58% → 20.17% (-2.0%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| code_index.build.and_publish             | 2 → 2 (+0.0%)              | 385.73ms → 384.68ms (-0.3%)    | 393.74ms → 393.22ms (-0.1%)    | 771.46ms → 769.36ms (-0.3%)     | 19.82% → 19.58% (-1.2%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| query.artifact.prepare_pages             | 13 → 13 (+0.0%)            | 36.91ms → 36.76ms (-0.4%)      | 52.99ms → 52.43ms (-1.1%)      | 479.84ms → 477.92ms (-0.4%)     | 12.33% → 12.16% (-1.4%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| code_index.chunk.index_file              | 276 → 276 (+0.0%)          | 1.72ms → 1.70ms (-1.2%)        | 2.53ms → 2.48ms (-2.0%)        | 473.84ms → 470.37ms (-0.7%)     | 12.17% → 11.97% (-1.6%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| query.artifact.finalization.advance_wake | 14 → 14 (+0.0%)            | 32.07ms → 32.20ms (+0.4%)      | 207.36ms → 206.57ms (-0.4%)    | 448.92ms → 450.78ms (+0.4%)     | 11.53% → 11.47% (-0.5%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| code_index.extract.parser_artifact       | 276 → 276 (+0.0%)          | 1.60ms → 1.58ms (-1.2%)        | 2.39ms → 2.38ms (-0.4%)        | 440.95ms → 437.02ms (-0.9%)     | 11.33% → 11.12% (-1.9%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| query.artifact.batch.parallel_prepare    | 13 → 13 (+0.0%)            | 29.26ms → 29.21ms (-0.2%)      | 41.94ms → 41.65ms (-0.7%)      | 380.40ms → 379.75ms (-0.2%)     | 9.77% → 9.66% (-1.1%)        |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| query.artifact.batch.commit              | 13 → 13 (+0.0%)            | 24.62ms → 28.02ms (+13.8%)     | 34.01ms → 40.80ms (+20.0%)     | 320.04ms → 364.30ms (+13.8%)    | 8.22% → 9.27% (+12.8%)       |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| code_index.chunk.build                   | 276 → 276 (+0.0%)          | 1.07ms → 1.04ms (-2.8%)        | 1.55ms → 1.50ms (-3.2%)        | 295.63ms → 286.66ms (-3.0%)     | 7.59% → 7.29% (-4.0%)        |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+
| code_index.chunk.identify                | 276 → 276 (+0.0%)          | 1.06ms → 1.03ms (-2.8%)        | 1.54ms → 1.48ms (-3.9%)        | 291.84ms → 282.98ms (-3.0%)     | 7.50% → 7.20% (-4.0%)        |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+---------------------------------+------------------------------+

Generated with hotpath-rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants