Priority and scope
P1 — potential data-loss path in legacy receipt compatibility. Follow-up to #642, #644, and #647; do not reopen their completed modern-receipt work.
Audited commit: b435f7b9179271cbff81d3e40d14ee342cbd65dd (still main when this issue was filed).
Evidence level: code-derived counterexample. The reviewer did not run the proposed regression and has not reproduced deletion of unrelated data.
Problem
Modern receipt.state.roots pass through inspectInstalledStateOwnership(). The compatibility branch in cursorLocalData() instead falls back from receipt.stateRoot to resolveInstalledStateRoot(destination, 'cursor', environment, home). When any receipt exists and the resulting location has source === 'derived', the path is added to the confirmed recursive-purge set.
Supported old receipts can omit both state and stateRoot. The resolver derives a default from the uninstaller's current XDG_STATE_HOME, rather than an installation-recorded location. Changing that environment can therefore change which external directory is selected for deletion without recorded association to the original install. This is the legacy XDG/default-location case, not the already-covered modern recorded-root environment-drift case.
Source evidence
- uninstall.ts:
cursorLocalData() selects fallback roots; uninstallCursorLocal() recursively removes the resulting purge paths.
- state-root.ts:
installedUserDataStateRoot() and resolveInstalledStateRoot() derive the current location; modern roots use independent ownership inspection.
- receipt.ts:
receiptFromDocument() accepts supported receipts lacking historical state fields.
Deterministic regression
- Install/create a valid supported legacy receipt without
state or stateRoot.
- Retain an original state directory, then change
XDG_STATE_HOME before uninstall.
- Put an unrelated sentinel in the newly derived directory.
- Request the normal confirmed plugin-data purge.
- Assert the unproven directory and sentinel survive, and the report does not claim that the original state was purged.
Acceptance
Test owners
Extend packages/agent-bundle/tests/uninstall.test.ts and the existing generated-installer/Doctor suites. Use isolated temporary homes and sentinel files, not real operator data.
Non-goals
No automatic adoption/deletion of historical external directories, general storage-migration service, relaxed purge confirmation, or changes to plugin-owned domain persistence.
Priority and scope
P1 — potential data-loss path in legacy receipt compatibility. Follow-up to #642, #644, and #647; do not reopen their completed modern-receipt work.
Audited commit:
b435f7b9179271cbff81d3e40d14ee342cbd65dd(stillmainwhen this issue was filed).Evidence level: code-derived counterexample. The reviewer did not run the proposed regression and has not reproduced deletion of unrelated data.
Problem
Modern
receipt.state.rootspass throughinspectInstalledStateOwnership(). The compatibility branch incursorLocalData()instead falls back fromreceipt.stateRoottoresolveInstalledStateRoot(destination, 'cursor', environment, home). When any receipt exists and the resulting location hassource === 'derived', the path is added to the confirmed recursive-purge set.Supported old receipts can omit both
stateandstateRoot. The resolver derives a default from the uninstaller's currentXDG_STATE_HOME, rather than an installation-recorded location. Changing that environment can therefore change which external directory is selected for deletion without recorded association to the original install. This is the legacy XDG/default-location case, not the already-covered modern recorded-root environment-drift case.Source evidence
cursorLocalData()selects fallback roots;uninstallCursorLocal()recursively removes the resulting purge paths.installedUserDataStateRoot()andresolveInstalledStateRoot()derive the current location; modern roots use independent ownership inspection.receiptFromDocument()accepts supported receipts lacking historical state fields.Deterministic regression
stateorstateRoot.XDG_STATE_HOMEbefore uninstall.Acceptance
--keep-datafollowed by purge cannot manufacture ownership retrospectively.src/install/surface.ts; keep its policy aligned with package CLI uninstall.Test owners
Extend
packages/agent-bundle/tests/uninstall.test.tsand the existing generated-installer/Doctor suites. Use isolated temporary homes and sentinel files, not real operator data.Non-goals
No automatic adoption/deletion of historical external directories, general storage-migration service, relaxed purge confirmation, or changes to plugin-owned domain persistence.