Skip to content

Legacy uninstall must not infer external state purge ownership from the current environment #685

Description

@ScriptedAlchemy

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

  1. Install/create a valid supported legacy receipt without state or stateRoot.
  2. Retain an original state directory, then change XDG_STATE_HOME before uninstall.
  3. Put an unrelated sentinel in the newly derived directory.
  4. Request the normal confirmed plugin-data purge.
  5. Assert the unproven directory and sentinel survive, and the report does not claim that the original state was purged.

Acceptance

  • Reproduce or disprove the compatibility-path counterexample with a regression before changing behavior.
  • Recorded, verified ownership remains the basis for external deletion. Missing historical location/ownership reports retained or unproven, not inferred purge.
  • Current environment/home changes cannot redirect legacy cleanup.
  • --keep-data followed by purge cannot manufacture ownership retrospectively.
  • Plan and execution agree on retained versus removable paths, and Doctor explains the distinction.
  • Check the equivalent supported generated-installer legacy path in src/install/surface.ts; keep its policy aligned with package CLI uninstall.
  • Preserve modern per-server roots, relative-cwd behavior, foreign-marker/shared-root protection, and canonical-path/symlink guards.
  • Update English/Chinese lifecycle documentation and diagnostics where behavior changes.

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions