Skip to content

Installer: deterministic cleanup environment and residual version-folder backup (#883) - #1046

Merged
iamfatness merged 9 commits into
mainfrom
codex/rc-883
Oct 25, 2026
Merged

iamfatness merged 9 commits into
mainfrom
codex/rc-883

Conversation

@iamfatness

@iamfatness iamfatness commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Closes #883

What was wrong

The Get-FileHash failure: reproduced. In Windows PowerShell 5.1, Get-FileHash is a script function that module auto-loading has to find. Compiled cmdlets such as ConvertTo-Json are always loaded, which is why only this one command failed. When setup is launched from a PowerShell 7 shell, the cleanup helper inherits PS7's PSModulePath. nsExec passes it through unchanged, and 5.1 then binds Get-FileHash to PS7's Core-only Utility module. The result is exactly the issue text: "The term 'Get-FileHash' is not recognized". I reproduced it through a real nsExec::ExecToStack, on both the 32-bit (SysWOW64) and 64-bit hosts. None of these reproduce it: empty, missing or stray module paths, a corrupt analysis cache, or __PSLockdownPolicy. This is a proven mechanism, not the proven cause of the 2026-10-13 incident. No PowerShell 7 exists on the dev box (the repro borrowed PS7 7.4.6's Modules from a downloaded zip), and the environment that launched that install was never captured.

Stale folders: still open on main. A read-only measurement on the dev box found 32 unregistered version folders: 29 holding only .camera-desktop-registration-before.json (it was missing from the inline metadata list), 2 holding Recordings\ (12 files), and 1 full unregistered payload. The registered pass enumerates uninstall keys, so it cannot see any of them.

What changed

  • Pinned environment (owner ruling D5). The helper resets PSModulePath for its own process only. It then imports Utility, Management and CimCmdlets by absolute path and checks that every command it uses resolves. Every run writes a diagnostics block (bitness, language mode, $PSHOME, inherited and effective path) to %LOCALAPPDATA%\CoreVideoPro\installer-logs\<release>-retire.log. If a command does not resolve, the error names it.
  • One metadata list, Get-OwnedInstallMetadataNames, used by both passes.
  • Residual sweep. It runs after the registered pass. It never enters a reparse point, skips running versions and any folder a camera registration points into, and never deletes recursively.
  • Operator content is moved, never deleted (owner ruling D4). Files go to %LOCALAPPDATA%\CoreVideoPro\backups\retired-install-remnants-<yyyyMMdd-HHmmss>-<id>\<release>\..., on the same volume only. backup-manifest.json (source, destination, length, sha256, status) is written before the first move. A name collision gets a numeric suffix. Any failure keeps that folder and records the reason.
  • Report. previous-installations-cleanup.json is now {summary, registered, residual}, with a copy in installer-logs. The last stdout line counts folders, not uninstall entries.
  • Backup always reported. Whenever any file was moved, even from a folder that then failed (for example on a locked file), the report carries the backup folder. The "MOVED, not deleted, to:" line is printed first, because nsExec keeps only the head of stdout.
  • Exit codes. 0 = clean. 2 = clean, and operator files were moved; setup shows an information box naming the backup folder. 1 = something was retained; setup shows a warning with the summary and log path.
  • Installer. It stays 32-bit NSIS with the same launch, plus -ReleaseId. /SD IDOK is kept.

Tests

Test 64-bit host 32-bit SysWOW64 host
Test-PreviousInstallations.ps1 23 checks pass 23 checks pass
Test-PreviousInstallations.ps1 -WithoutHashCommand 23 pass 23 pass
Test-ResidualInstallations.ps1 (new) 73 pass 73 pass
Test-CleanupEnvironment.ps1 (new; runs the real helper in 64-bit and 32-bit child hosts) 112 pass 112 pass

With the Get-FileHash call restored and the pin removed (the Task 1 helper, run with the planner module from 91db55bf^), the suite fails with the issue text: the old version is retained with "Get-FileHash is not recognized". That proves the test bites. It embeds PS7 7.4's Utility manifest verbatim, because a hand-trimmed look-alike does not reproduce the failure. When a real PS7 is installed, as on the CI image, it also uses that Modules directory. Both new tests run in CI under native-shell-windows.

installer.nsi compiles with makensis (exit 0) against a stub staging tree. The newest local beta zip predates #1033, so package-alpha-installer.ps1 could not package it.

Not done: installed acceptance (plan Task 7, owner)

The exact steps are in docs/superpowers/plans/2026-10-23-installer-cleanup-883.md under "Task 7 owner instructions". In short:

  1. Inventory the install root before the upgrade.
  2. Install the next beta containing this PR, interactively from Explorer.
  3. Require all of these: exactly one version folder and one uninstall entry; summary.retained = 0; a backup manifest listing the 12 recording files with matching SHA-256; a retire log showing is64Bit=False and the pinned path; shortcuts and camera ownership pointing at the new install; settings hashes unchanged.

A dry run with -InspectOnly against the real root (read-only) gave 32 residual plans, all ready: 29 metadata-only, 2 with recordings (10 + 2 files), and 1 with 875 proven payload files and 0 unknown.

Spec (with rulings D4/D5 and the Task 1 findings) and plan are in docs/superpowers/. docs/reference/installer.md and the BACKLOG #883 row are updated.

🤖 Generated with Claude Code

iamfatness and others added 9 commits October 24, 2026 03:25
Reproduces the original failure: a Windows PowerShell 5.1 child that inherits a
PowerShell 7 PSModulePath cannot resolve Get-FileHash. Runs the real helper from
the 64-bit and 32-bit hosts against throwaway fixtures (fake LOCALAPPDATA, HKCU
test namespace), fails against the pre-fix module, passes on main. Adds test-only
hooks for the registry root, Start-menu folder and camera owner key. Copies in
the spec and plan with owner rulings D4/D5 and the Task 1 findings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…#883)

Remove-PreviousInstallations.ps1 now resets PSModulePath for its own process
(owner ruling D5), imports Utility/Management/CimCmdlets by absolute manifest
path, proves every command it needs resolves, and logs the diagnostics block
(PS version, bitness, language mode, PSHOME, inherited and effective path) to
%LOCALAPPDATA%\CoreVideoPro\installer-logs\<release>-retire.log. A failure names
the command and is logged before it is rethrown (exit 1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Get-OwnedInstallMetadataNames is now the single metadata list (adds the
.camera-desktop-registration-before.json that stranded 29 folders) and the
registered retirement uses it. Get-ResidualInstallationPlan classifies every
file of an unregistered version folder as metadata, hash-proven payload or
unknown, skips running versions and the folder the camera points into, and
retains (never enters) anything with a reparse point. Read-only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ing it (#883)

Remove-ResidualInstallation (owner ruling D4) moves every unknown file of a
residual version folder into a dated retired-install-remnants-<stamp>-<id>
folder, same volume only, with backup-manifest.json written before the first
move; then deletes proven payload and owned metadata one file at a time and
removes empty directories non-recursively. Collisions get a numeric suffix;
any failure keeps the folder retained with the reason. -LeaveUnknown keeps
operator content in place for manual runs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Remove-PreviousInstallations.ps1 runs the residual sweep after the registered
retirement, writes previous-installations-cleanup.json as
{summary, registered, residual} (copy in installer-logs), and prints one line per
retained or left-in-place folder, the backup folder when files were moved, and a
final 'retired / backed up / retained' count of folders. Exit 0 clean, 2 clean
with operator files moved, 1 retained. installer.nsi passes -ReleaseId and shows
the backup location in an information box (owner ruling D4) or the summary in
the existing warning; /SD IDOK kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sts in CI

installer.md records the reproduced root cause, the pinned environment and log,
the shared metadata list, the residual sweep and backup layout (rulings D4/D5),
the report schema and exit codes. CI native-shell-windows runs
Test-ResidualInstallations.ps1 and Test-CleanupEnvironment.ps1. BACKLOG #883 now
says what shipped and that installed acceptance (plan Task 7, owner) is open; the
plan carries exact owner instructions for it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he repro proves (#883)

Review of #1046: a folder that moved some operator files and then failed (for
example on a locked file) is retained with moved > 0; summary.backupFolder,
filesBackedUp and the 'MOVED, not deleted, to:' line now come from moved > 0
regardless of status, and that line is printed before the Retained lines because
nsExec keeps only the head of stdout. New end-to-end case: partial move then
failure carries the backup folder in both stdout and the JSON (fails on the old
reporting). The 32-bit retire log notes that System32 entries resolve to
SysWOW64. Docs: the suite bites with the Get-FileHash planner restored and the
pin removed; PS7 inheritance is a proven mechanism, not the proven cause of the
2026-10-13 incident (no PowerShell 7 on the dev box).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@iamfatness
iamfatness merged commit 9bc7410 into main Oct 25, 2026
16 checks passed
@iamfatness
iamfatness deleted the codex/rc-883 branch October 25, 2026 11:13
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.

Installer cleanup subprocess cannot resolve Get-FileHash and retains prior version

1 participant