Files
DeepSeek-Reasonix/scripts
b8c82972f2 Harden Guard repair preview confirmation / 加固 Guard 修复预览确认绑定 (#6973)
* fix(guard): bind repair apply to confirmed preview

* Harden Guard preview confirmation binding

Problem: non-interactive apply-plan could accept a bare --yes, while preview IDs omitted derived-state contents and full pending-update identity.

Root cause: confirmation hashes covered only display previews and were checked once before the plan loop, so state-dependent actions could drift after confirmation.

Fix: require a prior preview ID for non-interactive apply-plan, bind opaque file and transaction state, re-check each action immediately before execution, and preserve unrelated multi-action behavior. Update English and Chinese recovery docs.

Verification: go test ./internal/repair ./cmd/reasonix-guard; go test -race ./internal/repair; go vet ./...; go test ./...; git diff --check.

Co-authored-by: SivanCola <[email protected]>

* Serialize confirmed Guard repair mutations

Problem: preview IDs were rechecked before mutation, but another process could still replace a target during the final rename window or race on last-repair.json, losing or mixing undo records.

Root cause: repair actions lacked a shared transaction lock and per-target cross-process locks, while confirmed snapshot/config bytes were read again after approval and replacement writes could overwrite a concurrent recreation.

Fix: serialize repair bookkeeping before canonical target locks, revalidate confirmed state inside the lock and around renames, publish restored content with create-only atomic writes, bind complete pending-update identity, preserve concurrent writers, and keep undo under the same lock order. Add focused coverage for wait/recheck, rename windows, snapshot bytes, unreadable LKG nodes, transaction serialization, and rollback identity.

Verification: go test ./internal/fileutil ./internal/config ./internal/repair ./cmd/reasonix-guard; go test -race ./internal/fileutil ./internal/repair; go vet ./...; env -u DEEPSEEK_API_KEY go test ./...; Windows test-binary cross-compilation for internal/repair and cmd/reasonix-guard; git diff --check.

Co-authored-by: SivanCola <[email protected]>

* Normalize Windows repair lock test paths

Problem: the Windows CI test waited for a raw backslash path while the lock hook receives the canonical lower-case slash form, so the test timed out before exercising the recheck.

Root cause: the regression test compared platform-specific input text instead of the production lock key representation.

Fix: normalize test hook paths with the same Windows case and separator rules used by repair mutation locks.

Verification: focused repair lock tests pass on macOS; Windows amd64 test-binary compilation passes.

Co-authored-by: SivanCola <[email protected]>

* test(jobs): remove StartForSession race

Keep the worker blocked while assertions inspect its state and protect job fields with the existing mutex. This prevents the race job from observing concurrent completion writes when validating accepted input.

Co-authored-by: PaulDing98 <[email protected]>

* fix(repair): bind target identity into Guard preview confirmation

Problem:
- previewId hashed only exported preview fields and content-based state IDs,
  so identical reasonix.toml content in two project roots produced the same
  confirmation and apply-plan --yes could mutate the unconfirmed project.
- Mutation locks used Abs/Clean only, so symlink path aliases (and macOS
  case variants) took different locks for the same real node.
- rollback_update bound only the pending transaction JSON and skipped target
  locks, so a stable pending update could still roll back an unconfirmed
  live release unit.
- When a target was recreated after quarantine rename, the move was not
  recorded in last-repair, so UndoLastRepair could restore an unrelated prior
  repair.

Root cause:
- Confirmation identity omitted canonical target identity; lock keys and
  rollback binding did not converge on real paths or live release-unit state;
  post-rename failure paths returned before persisting the already-moved node.

Fix:
- Embed an irreversible canonical target identity in per-file state IDs used
  by previewId, while verifying quarantined nodes against the original path
  identity.
- Resolve existing symlink parents and case-fold on Windows/macOS when
  building mutation-lock keys and target identities.
- Bind current release-unit file states into rollback preview state, take
  shared target locks during prepare/rollback, and re-check under those locks.
- Persist the quarantine/move transaction when a concurrent recreate is
  detected so undo restores the confirmed node and retains the rewrite as redo.

Verification:
- go test ./internal/repair/ -count=1
- go test -race ./internal/repair/ ./internal/fileutil/ -count=1
- go test ./cmd/reasonix-guard/ -count=1
- go vet ./internal/repair/

* fix(repair): close symlink, app-bundle, and macOS handoff confirmation gaps

Problem:
- canonicalRepairPath EvalSymlinks'd the full path, so two leaf symlinks
  sharing a referent produced the same previewId, and renaming a confirmed
  symlink leaf changed its identity and made apply fail.
- App-bundle rollback bound only directory kind/mode for the live .app and
  omitted the backup tree, so interior executable drift still accepted an
  old previewId and rolled back.
- macOS self-update held mutation locks only while writing the pending
  transaction; the actual mv/ditto ran in a shell helper that ignored the
  shared flock, racing Guard rollback into mixed bundles.

Root cause:
- Leaf nodes are what Lstat/Rename mutate, but identity resolved through them;
  directory release units need tree digests; handoff and repair used different
  processes without a shared lock holder over the swap window.

Fix:
- Resolve only parent-directory symlinks and reattach the original leaf name
  for mutation locks and preview target identity.
- Bind full tree digests for directory release-unit nodes and include backup
  paths in pending-update preview state.
- Replace the macOS shell handoff with a detached self-subprocess that waits
  for the old PID, then holds LockRepairMutations around the directory swap.
- Export LockRepairMutations/Timeout for desktop handoff and add regressions.

Verification:
- go test ./internal/repair/ -count=1
- go test -race ./internal/repair/ -count=1
- go test ./cmd/reasonix-guard/ -count=1
- cd desktop && go test -count=1 -run TestMacUpdate .
- go vet ./internal/repair/ and desktop

* fix(repair): secure updater claims and path case locks

Problem:
The detached macOS updater trusted filesystem paths from command-line arguments, and repair identities folded path case by operating system rather than by the target filesystem.

Root cause:
The handoff only checked that a pending-update file existed before swapping bundles. Path canonicalization assumed every macOS and Windows filesystem was case-insensitive.

Fix:
Persist optional handoff paths and owner identity in the pending transaction, claim them under strict pending and target locks, revalidate the staged bundle before swapping, and remove filesystem paths from child arguments. Detect case sensitivity from the actual macOS directory or Windows per-directory flag before folding lock and preview identities.

Verification:
- go test ./internal/repair/ -count=1
- go test -race ./internal/repair/ ./internal/fileutil/ -count=1
- go test ./cmd/reasonix-guard/ -count=1
- cd desktop && go test -count=1 -run TestMacUpdate .
- cd desktop && go test -race -count=1 -run TestMacUpdate .
- go vet ./internal/repair/
- cd desktop && go vet .
- GOOS=windows GOARCH=amd64 go test -c ./internal/repair

* fix(repair): scope case probe helper by platform

Problem:
The Linux golangci-lint job reported existingRepairPathParent as unused.

Root cause:
The helper lived in a common file while its only callers were excluded from Linux builds by Darwin and Windows build tags.

Fix:
Move the helper into a Darwin-or-Windows platform file so it is compiled exactly where the filesystem case probes use it.

Verification:
- golangci-lint run --timeout=5m
- go test ./internal/repair/ -count=1
- cd desktop && go test -count=1 -run TestMacUpdate .
- GOOS=windows GOARCH=amd64 go test -c ./internal/repair

* fix(repair): preserve verified rollback recovery

Problem:
- File rollback could install backup bytes that changed after preflight validation.
- A failed macOS backup restore still cleared pending recovery state and cleanup material.
- NFC and NFD aliases on APFS produced different repair lock and preview identities.

Root cause:
The staged-copy hash was ignored, macOS rollback cleanup was unconditional, and canonical repair paths handled case aliases without filesystem-aware Unicode normalization.

Fix:
Verify the staged rollback bytes against the transaction hash before mutating live targets. Make macOS rollback compensating and retain pending, backup, and staging state when restoration fails. Normalize repair identities to NFD only on APFS and HFS while preserving distinct names on other filesystems.

Verification:
- go test ./internal/repair/ -count=1
- go test -race ./internal/repair/ ./internal/fileutil/ -count=1
- go test ./cmd/reasonix-guard/ -count=1
- cd desktop && go test ./... -count=1
- cd desktop && go test -race -count=1 -run TestMacUpdate .
- go vet ./internal/repair/
- cd desktop && go vet .
- golangci-lint run --timeout=5m
- GOOS=windows GOARCH=amd64 go test -c ./internal/repair

* Harden transactional updater handoffs

Problem:
Repair and self-update paths could continue without a pending-state lock, apply a prepared update after its transaction or release unit changed, or restore an unverified macOS bundle tree. Linux also persisted a CLI sibling that transaction validation rejected.

Root cause:
Pending transitions used a best-effort unbounded lock, file updaters locked paths without claiming the exact prepared transaction, and app-bundle handoff/rollback validation did not bind both the staged tree and original backup tree across the rename window.

Fix:
Use bounded fail-closed pending locks and a consistent pending-to-target lock order. Claim Linux and Windows replacements by version, creation identity, platform, and canonical release-unit paths. Bind macOS staging, original, installed, and backup trees; recover rejected handoffs only when the original bundle is still verified; protect backup paths and reject symlink escapes. Add focused concurrency, drift, rollback, compatibility, and cross-platform tests.

Verification:
- go test ./internal/repair/ ./internal/fileutil/ ./cmd/reasonix-guard/ -count=1
- go test -race ./internal/repair/ ./internal/fileutil/ ./cmd/reasonix-guard/ -count=1
- cd desktop && go test ./... -count=1
- cd desktop && go test -race ./... -count=1
- go vet ./... and cd desktop && go vet ./...
- golangci-lint run --timeout=5m ./...
- Windows amd64/arm64 test-binary compilation for repair, desktop, and update-helper

Co-authored-by: SivanCola <[email protected]>

* Complete fail-closed repair and updater transactions

Problem:
Repair, undo, and self-update entry points could act on state that changed while waiting for locks. File-update completion also needed a crash-safe installed-unit binding, and platform handoffs could execute mutable or incompletely claimed payloads.

Root cause:
Invocation identity was sampled too late or incompletely, mutable pending state doubled as installed state, cleanup paths did not consistently compare ownership, and Windows/macOS handoffs did not bind every source, backup, staging, and release-unit transition.

Fix:
Bind every mutating invocation before lock waits and recheck it under a pending-to-target lock order. Persist create-only installed-state sidecars, preserve recovery material on ambiguity, use atomic no-replace publication, validate complete app and file trees, isolate and hold the verified Windows installer through execution, and make rollback/undo crash-retryable.

Verification:
go test ./internal/repair ./internal/fileutil ./cmd/reasonix-guard -count=1
cd desktop && go test ./... -count=1
Windows amd64 and arm64 update-helper test-binary compilation
gofmt -l internal/repair desktop
git diff --check

Co-authored-by: SivanCola <[email protected]>

* Document fail-closed legacy update recovery

Problem:
Recovery documentation described additive update state as generally backward compatible and a Linux comment still described the obsolete mutable-pending design.

Root cause:
The text was not updated when installed release-unit state moved to a transaction-unique sidecar and legacy destructive actions began requiring explicit ownership proof.

Fix:
Document immutable pending state, crash-safe sidecar recovery, and the distinction between readable legacy transactions and authorization for handoff, healthy commit, or app-bundle rollback.

Verification:
git diff --cached --check
Focused repair and updater tests, full race suites, vet, lint, and cross-platform compilation passed against the merged main-v2 baseline.

Co-authored-by: SivanCola <[email protected]>

* Close updater execution and compensation races

Problem:
- Generic failure-marker writes could let a stale invocation overwrite a newer transaction marker.
- Legacy app rollback could move a symlink backup before rejecting it, and file publish could leave a tampered staged node live.
- Windows self-update could execute a helper that changed after transaction prepare, overwrite a concurrently allocated cache path, or add a portable alias that was absent before.
- macOS updater commands were resolved through PATH.

Root cause:
Mutation ownership was not rebound under the pending lock at every public entry point. Helper validation proved only PE architecture, staged-file verification followed symlinks, and helper cache publication used replace semantics.

Fix:
Rebind generic failure markers after lock acquisition, reject non-directory app backups before mutation, require regular-file staged payloads, compensate failed publication by retaining the rejected node and restoring the verified prior file, and preserve pending recovery state. Bind the Windows helper to the exact prepared release-unit SHA-256, create a unique cache node without replacement, freeze it through process creation, preserve the original portable-alias shape, and use absolute macOS system command paths.

Verification:
- go test ./... -count=1
- go test -race ./internal/repair ./internal/fileutil ./cmd/reasonix-guard -count=1
- cd desktop && go test ./... -count=1
- cd desktop && go test -race ./... -count=1
- go vet ./... and cd desktop && go vet ./...
- golangci-lint 2.12.2 for root and desktop
- cache guard: 8/8 pass
- Windows amd64/arm64 repair, desktop, and update-helper test-binary compilation
- Linux amd64 repair test-binary compilation

Co-authored-by: SivanCola <[email protected]>

* Tidy desktop updater dependencies

Problem:
The desktop CI go-mod-tidy gate failed because the updater no longer imports github.com/minio/selfupdate, but the module files still retained that dependency and its stale transitive checksums.

Root cause:
The transactional updater implementation replaced the selfupdate package without running the desktop module tidy gate afterward.

Fix:
Remove the unused selfupdate requirement and the transitive checksum entries that are no longer reachable from the desktop module graph.

Verification:
- cd desktop && go mod tidy (idempotent)
- cd desktop && go vet ./...
- focused workspace-rebuild timing tests: 10/10 pass
- git diff --check

Co-authored-by: SivanCola <[email protected]>

* Harden prepared repair journals and Windows release-unit signing

Problem:
- Repair mutations could leave ambiguous undo state after a crash between
  rename and last-repair.json publication.
- Concurrent writers could replace a pending journal or cleanup node after
  ownership checks, turning cleanup into deletion of unowned material.
- Windows portable/installer updates did not bind signed payload identity
  through packaging, and Linux release-unit publication did not return
  exact installed-state receipts.

Root cause:
Undo bookkeeping treated last-repair.json as the only durable intent, cleanup
used path-based remove after verification, and Windows packaging trusted
unsigned payload layout without a create-only signed manifest contract.

Fix:
- Introduce create-only pending-repair.json write-ahead journals, promote or
  discard them under the repair lock, and fail closed on ambiguous recovery.
- Displace cleanup targets to unique siblings, re-verify identity, and never
  unlink a pending journal that changed before cleanup.
- Bind Windows signed payload manifests through packaging/signing and require
  exact installed-state receipts for Linux release-unit publication.

Verification:
- go test ./internal/repair -count=1 (pass)
- focused security coverage for prepared intent, pending cleanup ownership,
  legacy healthy/rollback fail-closed, and macOS staging cleanup

Co-authored-by: SivanCola <[email protected]>

* fix(ci): use resolved version for payload manifest

Problem: The Windows payload manifest step referenced a nonexistent ver step output, so actionlint rejected the release workflow.
Root cause: The build job receives the release version from needs.resolve.outputs.version, but the step used steps.ver.outputs.version.
Fix: Bind the signed payload manifest to the resolved release version output.
Verification: actionlint v1.7.7 and scripts/release-workflows.test.sh pass.

* fix(repair): bind prepared create ownership to published Windows modes

Problem:
- Windows CI rejected undo after a crash following snapshot create: the
  restored config file remained because CreatedStateID ownership failed.
- Desktop packaging contract tests still expected steps.ver.outputs.version
  after the release workflow switched to needs.resolve.outputs.version.

Root cause:
- Prepared create intents predicted FileMode 0o600, but Windows Lstat reports
  regular files as 0444/0666. Undo then treated the published file as an
  unowned concurrent write and left it in place.
- The packaging test was not updated with the resolved-version workflow fix.

Fix:
- Predict create-state IDs with platform-published file modes and rebind the
  pending journal to the exact on-disk node after AtomicCreateFile.
- Align the Windows signing packaging contract test with the resolve job
  version output.

Verification:
- go test ./internal/repair -count=1
- go test ./internal/repair -run TestPreparedCreateStateIDMatchesAtomicCreate,TestApplyRepairPlanPersistsMissingSnapshotTargetBeforeCreate
- cd desktop && go test -run TestWindowsReleaseSignsPayloadBeforeRepackaging .

Co-authored-by: SivanCola <[email protected]>

* fix(repair): reject prepared create ownership drift

Problem: A file replaced between AtomicCreateFile and ownership rebinding could be claimed by the repair transaction and removed by undo.
Root cause: The pending journal was rewritten to bind whatever state happened to be present after publication instead of proving it matched the precomputed owned state.
Fix: Keep the write-ahead ownership binding immutable and fail closed when the published path differs; add a regression test that preserves a concurrent replacement through undo.
Verification: focused crash/create tests, go test ./internal/repair, repair/fileutil race tests, Guard tests, go vet, and Windows repair test cross-compile pass.

---------

Co-authored-by: SivanCola <[email protected]>
Co-authored-by: PaulDing98 <[email protected]>
2026-07-29 16:52:28 +08:00
..