feat(migration): add gated Inno-to-Store migration preview - #1461
natalie-aguinaldo wants to merge 21 commits into
Conversation
Add protected migration records, read-only source inventory, completion-aware Inno safeguards, exact installation discovery, and disabled-by-default Store startup admission. Consent, adoption, and finalization remain future work. Local build, focused tests, and x64 fixture proof passed before checkpoint; structured review remains deferred. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a91896be-e75b-4a88-932e-61b78125ab4a
Keep the preview build-time gated and read-only. Consent only observes the Inno mutex and stops before adoption, completion, uninstall, or normal startup. Required validation and x64 packaged VM proof passed; structured review remains deferred. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a91896be-e75b-4a88-932e-61b78125ab4a
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a91896be-e75b-4a88-932e-61b78125ab4a
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a91896be-e75b-4a88-932e-61b78125ab4a
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a91896be-e75b-4a88-932e-61b78125ab4a
Hold the migration operation lock across Inno removal and cleanup, and re-detect the source under the completion lock. Use limited-information process queries to exclude proven unrelated processes while failing closed on uncertain source candidates. Add regression coverage and document the ownership guarantees. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a91896be-e75b-4a88-932e-61b78125ab4a
Keep completed-receipt reads independent of the current wall clock while retaining write-time timestamp checks, intent policy, and all binding and structural validation. Add backward-clock regressions through the codec, Windows PowerShell checker, and cleanup preservation path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a91896be-e75b-4a88-932e-61b78125ab4a
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
|
Codex review: needs real behavior proof before merge. Reviewed September 24, 2026, 3:07 AM ET / 07:07 UTC (Revision 24). ClawSweeper reviewWhat this changesAdds a gated preview for moving an Inno-installed Windows Companion to the Store package, with protected migration records, startup and uninstall safeguards, localized dialogs, tests, and release guidance. Merge readiness⛔ Blocked before merge - 8 items remain Keep this PR open. Current main lacks the migration handoff, and the branch has substantial real Windows proof. One observed receipt-loss path can still permit destructive gateway cleanup after migration, while the rollout and recovery contract needs maintainer approval. Priority: P2 Review scores
Verification
How this fits togetherThe Windows tray app and Inno uninstaller share settings, credentials, and a managed WSL gateway. The new migration records coordinate the Store app’s handoff and determine whether Inno may start or remove gateway state. flowchart LR
A[Inno installation and saved state] --> B[Store preview admission]
B --> C[Consent and source checks]
C --> D[Protected migration receipt]
D --> E[Inno startup and uninstall decision]
E --> F[Preserve or remove gateway]
D --> G[Store finalization]
Decision needed
Why: The staged release changes ordinary Inno behavior and defines a persistent destructive-cleanup authorization boundary; the contributor’s disclosed residual is not maintainer acceptance. Before merge
Findings
Agent review detailsSecurityNeeds attention: The new receipt-controlled uninstall boundary is generally hardened, but loss of the receipt can still authorize destructive cleanup after migration. Review metrics
Root-cause clusterRelationship: Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Keep Store migration gated, make completed-handoff preservation survive loss of its primary receipt, and validate the approved recovery path with signed production packages before rollout. Do we have a high-confidence way to reproduce the issue? Yes for the review finding: source shows that an absent completion receipt can return exit 0, and the contributor reports that result in a real Windows check. Migration itself is a new capability, not an existing-behavior bug. Is this the best way to solve the issue? Unclear. The staged, build-gated foundation is a reasonable shape, but the receipt-loss decision and signed production upgrade contract need resolution before it is the safest landing path. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 0e45bb673025. LabelsLabel changes: No label changes. Label justifications:
EvidenceSecurity concerns:
What I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (23 earlier review cycles; latest 8 shown)
|
Hold an Inno runtime reader until process exit and validate source activity under Store's exclusive migration lock. Share process inspection, preserve fail-closed startup guidance, and cover contention, exit, and completion retry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
Explain protected migration records, the Inno startup block after successful validation, manual source uninstall, and the Store restart requirement. Clarify retry guidance and cover all six locales with disclosure regression tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
Resolve additive architecture-ledger and six-locale resource conflicts while preserving migration safeguards and current-main chat changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
Remove action legends and refer directly to native Yes/No buttons in all six locales without changing consent or retry semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
…cesses Keep the existing 30-second deadline and parallel test execution. Run fixture scripts noninteractively, terminate timed-out child trees before teardown, and retain the original timeout when cleanup or diagnostic capture fails. Add synchronized parent/child regressions including an unreadable diagnostic log. This hardens the test harness without claiming to resolve the original CI-only timeout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
Replace the readiness-file handshake that hit a sharing violation on a Windows CI runner. Signal a named event after diagnostic output is written, and pass the child PID through stdout. This fixes the regression fixture only; the original cleanup timeout remains unresolved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
|
Global triage: NEEDS_HUMAN_TEST. Take confidence 20%; recommendation confidence 92%; effort large; risk high. Reviewed exact head |
Harden the Inno-to-Store migration safety path against outcomes that previously authorized destructive gateway cleanup on incomplete evidence. - Treat a present-but-unverifiable completion receipt as "preserve", not "absent". Only FileNotFoundException/DirectoryNotFoundException exit 0; every other validation failure exits 2. - Bound every child process wait in both shipped scripts via a shared Start-BoundedProcess helper built on ProcessStartInfo, and retain the checker's diagnostics instead of discarding them. - Launch the checker watchdog through the explicit System32 Windows PowerShell path, since $PSHOME under PowerShell 7 has no powershell.exe. - Narrow the destructive admission phase to Enter-DestructivePhase so pre-destructive failures report uncertainty rather than completion. - Route installer exit code 2 to preservation instead of a retry prompt, renumber internal failure codes to 102/103, and keep the cleanup script installed so the uninstaller no longer names a file it deletes. - Allow startup after a failed or refused startup preference, and show a user-visible notice when Windows durably refuses the startup task, since only the user can re-enable it. Validation: build.ps1; Shared 4087 passed; Tray 3098 passed; Connection 1072 passed. Watchdog timeout and admission-phase mutation proofs run locally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
Migration coordination is preview-only, but its lock sat outside the preview gate on two ordinary paths. An unreadable prepare.lock refused to launch the app and aborted the uninstaller outright, for users who never migrated. - Inno startup now blocks only when the lock is genuinely contended, which proves a live migration. Any other acquisition failure logs and continues, because an unreachable lock says nothing about migration. The completion receipt check below remains authoritative and still fails closed. - InitializeUninstall never returns False for inaccessible migration state. Only a contended lock stops an uninstall; otherwise removal proceeds and destructive gateway cleanup is suppressed instead. The reparse-point check stays sequential so a rejected path can never reach ForceDirectories. - Apply directory security on create or on drift rather than on every acquisition, so an ordinary launch no longer needs WRITE_DAC/WRITE_OWNER. - Make preservation self-contained: the notice now names the in-app removal path and the wsl --unregister command instead of pointing at documentation the uninstaller does not link, and the silent path logs the preserved distro and directory for enterprise audit. - Document the installer preservation case and why no override is offered. Validation: build.ps1; Shared 4087 passed; Connection 1075 passed; Tray 3100 passed. ActivationRouterTests passed 5/5 on this head, consistent with a baseline flake rather than a regression from this branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
…pers The checker exists because Add-Type can stall on a contended %TEMP% or an antivirus scan, and installer.iss waits for it with ewWaitUntilTerminated. Falling back to the inline check when the watchdog could not start reintroduced exactly that unbounded stall. Report the uncertain verdict instead; exit 2 already fails closed and preserves the gateway. The three uninstall helpers also carried uninsneveruninstall, but they run during usUninstall, which happens before Inno removes files. Generated app-state deletion only runs after successful gateway cleanup, so any uninstall that kept the local gateway, including the ordinary "No" choice unrelated to migration, left executable helpers behind in the former app directory. The retained-helper contract test pinned the flags it should have rejected, so it is inverted rather than deleted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
RequiresHardening skipped re-hardening whenever the DACL was protected and owned by the current user, without inspecting what those rules granted. A pre-existing store-migration directory can satisfy both conditions and still carry an Allow rule for an unrelated principal. That principal could delete completed.dpapi. Test-InnoMigration.ps1 maps a missing receipt to "no migration happened" and exits 0, which authorizes destructive local gateway cleanup during uninstall. The same receipt gates ordinary Inno startup. Before the hardening shortcut landed, SetAccessControl ran on every acquisition and would have removed such a grant. Restore that guarantee without reintroducing per-launch DACL churn by re-hardening only when a rule falls outside the owner, LocalSystem, and BuiltinAdministrators. RepeatAcquisition_LeavesAnAlreadyHardenedDirectoryUntouched used an Everyone: Read ACE as its no-rewrite canary, which is the grant this change must scrub. Its canary is now a trusted principal with distinct inheritance flags, so it still proves the absence of churn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
Real uninstall proof on a guest without WSL surfaced two pre-existing defects in the gateway cleanup helper, both dating to openclaw#591. Neither was reachable before, because earlier proof runs declined gateway removal and never executed this branch. wsl.exe writes UTF-16LE on many Windows builds while the redirected pipe is decoded as 8-bit, so captured output arrived NUL-interleaved. Every pattern in Test-DistroNotFound matched against that text and none could ever fire, leaving the "already unregistered" success path unreachable in practice. Strip NUL in the captured output instead of pinning an encoding that varies by Windows version. A host with no WSL, or with WSL but no distributions, cannot be holding our gateway. Treat both as nothing to remove rather than surfacing "OpenClaw could not remove the local WSL gateway. Exit code: 1", which offered Retry against a condition no retry can fix. The new test executes the real functions extracted from the script rather than asserting on source text, since a source-text contract cannot detect an encoding defect. Reverting either half fails it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
|
Addressing clawsweepers new comments |
The Inno uninstall checker decided whether to clean up gateway state based on a receipt file it did not verify it could trust. A non-trusted principal who could write the receipt (or its directory) could suppress or forge the cleanup decision. Trust is now established before the receipt is read: - Owner must be the current user, SYSTEM, or Administrators. - Any foreign Allow ACE that can mutate the receipt breaks authority. Read/list grants are tolerated, since installer.iss creates the state directory with plain ForceDirectories and inheritable read ACEs are common in managed fleets. - Generic rights (GENERIC_ALL, GENERIC_WRITE) count as mutating. They intersect no named FileSystemRights bit but the kernel maps them onto FILE_ALL_ACCESS / FILE_ADD_FILE, so a named-bit mask alone missed them. - Inherit-only ACEs are skipped; they do not apply to the object itself. When authority cannot be established the checker exits 2 and the uninstaller preserves state, rather than silently proceeding. Also replaces Get-Acl and Get-ChildItem with .NET types throughout. The uninstaller launches PowerShell in a constrained environment where Microsoft.PowerShell.Security fails to autoload, which made the check throw for every user and strand cleanup unconditionally. Residual, deliberately not sealed: a receipt file's own owner can grant itself DELETE and remove the receipt without altering the directory, which leaves nothing observable to the checker. Inherited Modify grants held by a domain administrators group will likewise strand cleanup rather than authorize it. Both fail closed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
Real VM proof on Windows 11 24H2 with WSL 2.7.14 showed every wsl.exe call in the gateway cleanup failing with exit 127: wsl.exe --list --quiet exited 127. /bin/sh: --list: not found wsl.exe matches its control flags against the raw command line and does not strip quotes, so a quoted "--unregister" is not recognized as a flag and is executed as a command inside the distro instead. The gateway distro was therefore never unregistered, and uninstall reported succeeded=false with exit 127. This regressed in 6a8aede, which replaced Start-Process -ArgumentList (an array, never quoted) with ProcessStartInfo (a single string) and quoted every argument to build it. The migration-checker call site was unaffected because it already passes bare flags and quotes only values. Quote only values that need it. The escaping rules for values that do need quoting are unchanged, including doubling trailing backslashes. Reproduced independently against real WSL on a developer host: wsl.exe --list --quiet -> exit 0, lists distros wsl.exe "--list" "--quiet" -> exit 127, /bin/bash: --list: not found Proof: real WSL2 gateway cleanup in a Hyper-V VM, before and after. before: exit 127, "Failed to unregister WSL distro", still registered after: exit 0, "Local WSL gateway removed", unregistered, unrelated bystander distro preserved, second run idempotent The new contract test executes the real function rather than asserting on source text, matching the existing WSL-less-host test. Source-text assertions cannot catch an argv-shape bug. Verified non-vacuous: the previous implementation turns --list into "--list" and fails it. Validation: build.ps1, Shared 4087 passed, Tray 3103 passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5c700f0-7fa2-46ed-bd1a-10a11521b63a
Related: #1374
PR 1 of 2: migration foundation and gated end-to-end preview. Production Store migration remains disabled in this PR. PR 2 is planned to complete #1374, including shipping UX and production enablement once release prerequisites and acceptance proof are satisfied.
Current-main integration and proof refresh:
1e504ceba488f04d616f12d086374c25847eaff6mergesmainatc7063081. Seven additive conflicts were resolved by preserving the migration and chat architecture rows and all six locales' resource entries. The full non-Dev Release Inno app and gated Debug MSIX were rebuilt after this commit, then exercised in the isolated Windows VM. Source tree:ff8c281a013e6f7ee1e979cf79fc50eaa8db1b67. Both earlier review fixes remain intact.Latest head:
71c91d05supersedese47197b6. Two commits follow it.f91e6895rejects foreign mutating grants on the uninstall receipt, including generic rights, and checks receipt ownership before trusting it.71c91d05fixes a regression this branch introduced in6a8aeded: the gateway cleanup quoted everywsl.exeargument, so control flags such as--unregisterwere executed as commands inside the distro and the gateway distro was never unregistered. Both are proven at runtime below.The long-standing "no registered WSL distro" gap is now closed. Earlier revisions could only prove the no-distro cleanup branch. A real registered WSL2 distro is now created, unregistered by the shipped helper, and confirmed gone, with an unrelated bystander distro preserved.
Product sources:
22db82746143542f3d69f69813f6884b43a3b780removes the extra action legends from both preview dialogs. The body now refers directly to the native Yes/No buttons in all six locales, preserving disclosures, default No, and behavior. Required validation passed on these sources; the packaged journey and both screenshots below were freshly captured from this committed head using test package2026.9.20.0. Source tree:da67d55bf6b3634a96841bedd8583034f39b8d30.What Problem This Solves
Inno and Store installations share user state and cannot safely operate as independent production copies. Inno uninstall can also remove the gateway that the Store app needs to preserve.
User Impact
Ordinary Store builds do not enter migration. An explicitly enabled Debug package exercises a same-user, matching-architecture handoff while preserving existing state in place.
Ordinary Inno builds include the completion guard and uninstall-preservation safeguards. Normal Inno startup acquires a read-shared migration handle before checking completion and retains it until process exit. A contended handle proves a live migration and blocks startup. An unreadable one does not, so startup continues: the completion receipt check stays authoritative and still fails closed. A valid completed receipt blocks normal Inno startup; uninstall preserves generated state. Uninstall is never refused because migration state is unreadable. Only a contended lock stops it, and any other uncertainty suppresses destructive gateway cleanup instead of cancelling removal.
Why This Change Was Made
Review fixes
e547733c: normal Inno retainsprepare.lockthrough process exit, including failed startup/shutdown. Store preparation/completion recheck source activity across sessions while holding the exclusive file lock. Busy completion supports Retry. The session-local mutex is no longer treated as sufficient cross-session ownership.46fd8b6e: consent in all six locales discloses protected records, the Inno startup block, required manual uninstall, Store restart, preservation, and no automatic uninstall. Close/Retry no longer promises that nothing has changed.7aef6b20: the preservation checker no longer falls through to an unbounded inline watchdog when its own watchdog cannot start; it warns and exits2(uncertain, preserve). Uninstall helpers no longer carryuninsneveruninstall, so they are removed with the app instead of being stranded on disk.4c5899b8: the migration directory DACL is now scrubbed of foreign grants on every acquisition, and explicit ACEs on the receipt file are scrubbed too. A previous churn optimization had skipped re-hardening for any protected, owner-matching DACL without inspecting its rules, which left a pre-existing directory able to grantEveryone: FullControl. A foreign principal deletingcompleted.dpapiwould have been read as an absent receipt and allowed destructive gateway cleanup.e47197b6: fixes a pre-existing uninstall defect that predates this PR (introduced in Add uninstall choice for local WSL gateway #591). Two problems, both in the gateway cleanup helper this PR already reworks:wsl.exewrites UTF-16LE on many Windows builds while the redirected pipe was decoded as 8-bit, so captured output arrived NUL-interleaved. Every pattern inTest-DistroNotFoundwas matchingT\0h\0e\0...and none could ever fire, leaving the "already unregistered, nothing to do" success path unreachable in practice.OpenClaw could not remove the local WSL gateway. Exit code: 1with a Retry button against a condition no retry can fix.Why it is in this PR rather than a separate one: it is the blocker for the required
windows-clean-installer-upgradeproof. The matrixremovecases exist specifically to exercise the no-distro cleanup branch, and that branch could not complete until this was fixed. It was invisible until now because earlier proof runs declined gateway removal and never executed the branch.How it was found: the real installer matrix on a WSL-less guest, not review. Neither ClawSweeper nor the human review caught it. The fix is covered by a test that executes the real functions extracted from the script rather than asserting on source text, because a source-text contract cannot detect an encoding defect; reverting either half fails it.
Maintainer decisions recorded
STORE_MIGRATION_PREVIEWgate. An Inno user who never opts into the preview can still have a completed Store migration, so gating the safeguard would let uninstall destroy preserved gateway state.wsl --unregister OpenClawGateway. The silent path now logs the leftover distro and directory so an enterprise admin reading the uninstall log has something actionable.Scope and review guide
InnoInstallationDetector, version policy, startup coordinator: no guessed source or production opt-inMigrationRecordCodec,MigrationInventory,MigrationPreparation: bounded same-user state, atomic records, source state preservedMigrationOperationLock,InnoSourceActivityVerifier, startup guard: process-lifetime reader versus exclusive Store writer; uncertain process inspection blocksinstaller.iss,Test-InnoMigration.ps1, cleanup helper: preservation check and destructive work cannot race completionApponly composes and retains lifetime ownershipGatewayRegistry,CredentialResolver, andAutoStartManagerremain canonical owners. There is no new node command, MCP tool, gateway client, or payload-relocation path.Follow-ups, intentionally not included
PR 2 (complete #1374): shipping UI, Open Installed apps, Inno Store-launch/consent handoff, graceful shutdown/manual fallback, activation/accessibility coverage, and production enablement after identifying a verified safe Inno release and satisfying release prerequisites and acceptance proof.
The fixture's
2026.9.5.0minimum is not a production compatibility claim. The eventual minimum must include the process-lifetime lock; process scanning alone cannot prevent an older nonparticipating binary starting after inspection.Evidence
Merged-source automated validation and the rebuilt, locally test-signed x64 packaged journey passed. Separate actual Release-app proof crossed Windows sessions 0 and 1. These are isolated local proofs, not official signed-release or Store-distributed acceptance.
Change Type
Scope
Required proof pools
windows-clean-installer-upgrade: passed, 8 of 8 cases onf91e6895. Every case reachedexitCode 0with visible uninstall completion, and the guest was returned to baseline with matching hashes. The real uninstaller invoked the changed preservation checker in all eight runs (Migration preservation check returned 0.). Inputs are official signedv2026.9.4and a full current-head unsigned local Release Inno installer. Because the installer is unsigned, this is not shipping-signature proof. This matrix ran onf91e6895; a rerun against71c91d05is pending and noted under Not verified / blocked.windows-wsl-gateway-e2e: passed, 3 of 3 cases on71c91d05. Real WSL2 distro registered, then removed by the shippedUninstall-LocalGateway.ps1: distro unregistered, install directory removed, unrelated bystander distro preserved, second run idempotent. Synthetic Alpine rootfs rather than a provisioned gateway, so gateway runtime behavior and live pairing are still not claimed.windows-winui-interactive: current-source English packaged consent/Retry captures and blocked Inno startup captures passed. Full shipping UX, localized layouts, and accessibility remain outstanding.windows-11-arm64: not verified / blocked. No native ARM64 host used.These are applicable host classes, not claims that maintainer-scheduled proof pools completed. No MXC execution, GPU, signing-policy, or Smart App Control change is claimed.
Validation
.\build.ps122db8274dotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csproj --no-restore22db8274sourcesdotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csproj --no-restore22db8274sourcesdotnet test .\tests\OpenClaw.Connection.Tests\OpenClaw.Connection.Tests.csproj --no-restore22db8274sourcesFullyQualifiedName~InnoMigrationContractTests|FullyQualifiedName~LocalizationValidationTests2026.9.22.0, rebuilt from committed22db82742026.9.20.0, built from committed22db8274git diff --checkCurrent head
71c91d05revalidation:.\build.ps1passed; Shared 4,087 passed / 33 skipped; Tray 3,103 passed; 0 failed. The Tray count rises by one againstf91e6895: a new contract test asserts thatwsl.execontrol flags are passed unquoted. It is verified non-vacuous, since the previous implementation turns--listinto"--list"and fails it.On
f91e6895:.\build.ps1passed; Shared 4,087 passed / 33 skipped; Tray 3,102 passed; Connection 1,104 passed / 1 skipped; 0 failed. Connection rises by 24 overe47197b6, covering foreign-owner rejection, generic-rights masks, inherit-only ACE handling, and receipt ownership.Earlier head
e47197b6revalidation:.\build.ps1passed; Shared 4,087 passed / 33 skipped; Connection 1,077 passed / 1 skipped; Tray 3,102 passed; 0 failed.ActivationRouterTestspassed 36/36 on five consecutive runs on this head, supporting a baseline-flake reading rather than a regression from this branch. New coverage pins the recovery contracts: contended versus unreadable lock handling, uninstall never cancelling on unreadable state, directory security applied only on create or drift, and the preservation notice carrying its own removal instructions.Clock regressions execute the real Windows PowerShell 5.1 preservation checker and isolated cleanup helper without changing the host clock. Completed receipts survive simulated backward-clock correction; corrupt records, mismatched bindings, and invalid intents remain rejected. Cross-process tests cover shared readers, exclusive contention, normal/crash handle release, and source restart between preparation and completion.
Historical failures are not hidden: the original pre-merge full Connection run had two checker failures, followed by isolated and full-suite passes. An initial cross-session-development Shared run timed out waiting for the Piper extractor fixture PID; it passed alone, then complete required validation reruns passed. The current merged-source runs above passed. Skips are not counted as proof.
Rubber-duck / code review
Earlier non-Codex whole-branch/scoped reviews found and drove fixes for uninstall/completion serialization, excessive process-query access, and clock-rollback receipt rejection. The two latest feedback fixes received direct source review and regression/runtime proof.
ClawSweeper revision 4 reviewed
46fd8b6e, accepted both code fixes and the refreshed runtime evidence, and reported no actionable code or security findings. It requested current-main conflict resolution, installed-user compatibility proof, and maintainer acceptance of the always-on safeguards.The additive merge resolution received direct review and exact three-way resource comparison. External structured-review attempt:
python .\.agents\skills\autoreview\scripts\autoreview --mode local --engine claude --no-web-search --no-tools .... The merge bundle exceeded the helper's size limit. The subsequent narrow Yes/No bundle passed that gate but was blocked by the unavailable Claude executable. No independent post-merge or Yes/No model-review result is claimed, and no Codex fallback was run. This is not a security-audit certification.Real behavior proof
Environment: disposable local IXPTools / Hyper-V Windows 11 x64 VM; same-user synthetic state; interactive non-elevated app. Provider: local Hyper-V. Cloud lease ID and run URL: N/A. Harnesses are session-local, not committed repository entry points.
Actual Inno runtime, across Windows sessions
Command:
pwsh -NoProfile -File run-cross-session-vm.ps1from an elevated host controller. The full non-Dev Release x64 self-contained unpackaged app was rebuilt from1e504ceb, launched in guest session 1, and checked from guest session 0. Output:cross-session-vm-results-r4.Six cases passed: normal startup with no receipt; corrupt-receipt compatibility; exclusive-owner blocked startup; valid-completion blocked startup; unreadable-receipt blocked startup; reparse lock-path rejection without modifying the target. A running app excluded the other session's exclusive handle while allowing uninstall-style readers. Handles were available after process exit. Normal-app cases used forced process exit; blocked dialogs exited by dismissal. Graceful full-app shutdown is not separately claimed.
This cross-session evidence is from the merge commit, not a rerun on
22db8274. The later change only alters Store preview copy/formatting, its tests, and documentation; the Inno runtime guard is unchanged.Actual packaged consent and migration journey
Commands:
pwsh -NoProfile -File build-migration-startup-preview.ps1, thenpwsh -NoProfile -File run-migration-startup-vm.ps1 -Resume.OpenClawFoundation.OpenClaw_2026.9.20.0_x64__rfcbke2p71se2, locally test-signed inside the VM and launched from its installed package location.22db82746143542f3d69f69813f6884b43a3b780. This is a post-commit rebuild, not a source-equivalence claim.1475B50CEDDA7AFE21DCF061064E9807E6DB18137FD4E9C9DA17E210E09DC036.6C98E61317C1BCB84D96DCA029DB0731424D3B075CB3FB52D3998E07F5890770. It uses current installer/codec/helper sources and a versioned test apphost, not an official released full installer.AutoStart=false, cleared records, and continued normal packaged startup. Process identity/logs, not a normal-window screenshot, establish that final step.Copied current packaged-run result (
startup-preview-results-20260923-002654):{ "declinePreservedSetupWithoutRecords": true, "restartPreservedReceipt": true, "blockedUninstallExitCode": 1, "exclusiveLockBlockedUninstall": true, "uninstallPreservedState": true, "sourceSettingsUnchanged": true, "preparedIntentWritten": true, "finalized": true, "success": true, "cases": ["consent-decline", "consent-retry", "restart-waiting", "finalize"] }Screenshots were visually inspected locally. They are GitHub-hosted attachments uploaded through the contributor's public fork, where the account has the push access required for attachment upload. No credentials, raw settings/identity files, or unrelated desktop content are published.
All three image URLs were verified after publication with unauthenticated HTTP 200 responses and SHA-256 matches against the inspected local captures.
Actual Inno install/uninstall matrix, eight cases
Command:
Run-InnoMatrixVm.ps1 -Execute -RunId 71c91d05-r1 -ExpectedManifestSha256 8260C3D0...from an elevated host controller against a disposable local Hyper-V guest. Payload pinned by manifest SHA2568260C3D0CBB4E33282A6DC684CC687677574D9983B15317EDE16079CD5BE6576, built from current head71c91d05. Output:results-71c91d05-r1. The guest summary recordscommit: 71c91d059fd4c9865c70e3975bfaeadb4c99aaa0, so the matrix is current-head rather than prior-head.8 of 8 cases passed. Each case reached
exitCode 0withvisibleCompletion: trueunder an elevated same-user interactive task, and the guest was restored to baseline with matching hashes (restoration: restored-with-matching-hashes).The real uninstaller invoked the changed preservation checker in all eight runs, evidenced by
Migration preservation check returned 0.appearing in every one of the eightuninstall.logfiles.fresh-autostart-false-retainfresh-autostart-false-removefresh-autostart-true-retainfresh-autostart-true-removeupgrade-autostart-false-retainupgrade-autostart-false-removeupgrade-autostart-true-retainupgrade-autostart-true-removePer-case evidence includes install logs, startup/registration state, UI screenshots, and UIA accessibility dumps for the uninstall confirmation and the gateway retain/remove choice.
upgrade-autostart-true-remove: the previous-release upgrade path with autostart enabled and the gateway remove choice. The real Yes/No gateway prompt is presented during uninstall and clicked by the harness.upgrade-autostart-true-remove: the same previous-release upgrade path reaches visible successful completion, exit code 0, no error dialog.The four
removecases each ran the real gateway cleanup helper end to end. The earliere47197b6-r1run exposed the pre-existing uninstall defect fixed ine47197b6: on the prior head the firstremovecase blocked on acould not remove the local WSL gatewaydialog and timed out. The71c91d05-r1run confirms that fix stays in place, with the same case passing with no error dialog.That run also demonstrated why the watchdog in
7aef6b20matters: on a WSL-less hostwsl.exeemits an interactive "Press any key to install... this prompt will time out in 60 seconds" prompt during uninstall. The 120 s bound contains it; without a bound it is an indefinite hang in an uninstaller.Deviations, stated plainly: the installer under test was unsigned, so this is not shipping-signature proof. Prerequisite install used an explicitly approved elevated step; first-time UAC remains unproven. The matrix guest has no registered WSL distro, so its
removecases prove the real no-distro cleanup branch; live distro deletion is proved separately below.Actual WSL gateway removal against a real registered distro
This closes the long-standing "no registered WSL distro" gap in the matrix above.
Command:
Run-GatewayProofVm.ps1 -Execute -RunId gateway-proof-r7against a disposable local Hyper-V guest with real WSL2 installed (wsl.2.7.14.0.x64.msi, SHA-256DB084E53...). Output:results-gateway-proof-r7. The proof invokes the shippedscripts/Uninstall-LocalGateway.ps1directly, which is exactly how Inno invokes it.3 of 3 cases passed on
71c91d05.registered0,stillRegistered: false,installDirRemoved: truebystanderOpenClawProofBystanderstill registeredidempotent0Registration state is read from the
Lxssregistry, the same key the product's ownership gating reads. Before cleanup:[ { "Name": "OpenClawProofBystander", "BasePath": "...\\OpenClawProofTray\\wsl\\OpenClawProofBystander" }, { "Name": "OpenClawProofGateway", "BasePath": "...\\OpenClawProofTray\\wsl\\OpenClawProofGateway" } ]After cleanup, only the bystander remains:
[ { "Name": "OpenClawProofBystander", "BasePath": "...\\OpenClawProofTray\\wsl\\OpenClawProofBystander" } ]Copied result summary:
{ "success": true, "stage": "complete", "wslVersion": 2, "cases": [ { "case": "registered", "pass": true, "exitCode": 0, "stillRegistered": false, "installDirRemoved": true, "reportedMessage": "Local WSL gateway 'OpenClawProofGateway' removed." }, { "case": "bystander", "pass": true, "stillRegistered": true }, { "case": "idempotent", "pass": true, "exitCode": 0 } ] }This proof caught a real regression that this branch introduced. The first run against a real distro failed every
wsl.execall:6a8aededon this branch replacedStart-Process -ArgumentList(an array, never quoted) with aProcessStartInfocommand string, and quoted every argument to build it.wsl.exematches its control flags against the raw command line without stripping quotes, so"--unregister"was passed through to the distro's shell and executed as a command. This was reproduced independently on a real WSL host outside the VM:wsl.exe --list --quietexits 0 and lists distros,wsl.exe "--list" "--quiet"exits 127.71c91d05narrowsConvertTo-ProcessArgumentto quote only values containing whitespace or quotes. After the fix, the same log reads:The regression is now locked by
GatewayUninstall_PassesWslControlFlagsUnquoted, which executes the real argument-building path rather than asserting on source text. It is verified non-vacuous: the previous implementation turns--listinto"--list"and fails the test.Deviations, stated plainly: the rootfs is a minimal Alpine image, not a provisioned gateway, so this proves cleanup and bystander preservation, not gateway runtime behavior or live pairing.
Uninstall-LocalGateway.ps1is invoked directly; SetupEngine ownership gating is covered by unit tests, not by this proof. The matrix cases above have not yet been rerun on71c91d05.Actual foreign-principal receipt authority, six cases
Command:
Invoke-ForeignReceiptProof.ps1from an elevated host controller. The proof creates a real local principal, applies real ACLs, and runs the shipped Windows PowerShell 5.1 preservation checker. Exit2means "preserve the gateway", exit0means "cleanup authorized".6 of 6 cases matched expectations.
cleanforeign-daclforeign-ownedresidualforeign-readforeign-genericGENERIC_ALLforeign-genericis the case that matters most: the raw mask on the ACE is0x001F01FF,0x10000000, which intersects no namedFileSystemRightsbit, yet the kernel maps it toFILE_ALL_ACCESS. A naive named-bit check would have passed it as harmless.Two limits are disclosed rather than hidden.
residualreturns0because nothing observable remains, so the checker genuinely cannot distinguish a deleted receipt from one that never existed. A domain administrator holding inherited Modify rights will likewise strand cleanup at exit2. Both are accepted properties of a same-user, filesystem-observable design, not defects the checker can close.Actual uninstall authority, real distro, control and refusal
Command:
Run-AuthorityProofVm.ps1 -Execute -RunId authority-proof-r2against a disposable Hyper-V guest. Real current-head Inno install, a real registered WSL2OpenClawGatewaydistro, and the real interactive uninstaller in both arms.25 of 25 assertions passed.
control-removed0authority-refused2The control arm exists so the refusal is not vacuous: the same harness, same installer, and same real distro demonstrably do perform a destructive unregister when authority checks out.
The foreign principal is genuinely distinct from the uninstalling user. The receipt owner is
...-1002(OpenClawAuthProof), while the signed-in user is...-1000, recorded with full SDDL inforeign-receipt.json.In the refusal arm the uninstaller logged its own decision and left recovery instructions:
The refusal is structural rather than a silently failed deletion:
noWslCleanupLogExists count=0, so the cleanup script was never invoked and nowslcall was attempted at all.HKCU\...\Lxssstill containsOpenClawGatewayafterward, and the install directory is intact.Control arm, the real destructive choice is offered, with No focused by default:
Refusal arm, same installer and same point in the flow, the destructive choice is replaced by an acknowledgement-only warning:
Disclosed limits. The fixture imports a minimal Alpine rootfs rather than a provisioned gateway runtime, so this proves registration and directory lifecycle for the exact
OpenClawGatewaytarget, not gateway runtime behavior. Both uninstalls ran through an elevated interactive task, matching the matrix harness shape.Existing gateway state survives Inno removal and is adopted by the Store build
Run on a clean Windows VM at head
71c91d05. Inno2026.9.23.0installed over a pre-existing gateway fixture, uninstalled with No to the local gateway prompt, then the current-head Store package installed and launched.fixture-had-statedurable-state-byte-identicalvolatile-churn-reportedopenclaw-tray.log,Logs\diagnostics.jsonl,store-migration\prepare.lock.distro-still-registereddistro-enterableopenclaw-distro-okstore-distro-registeredStore package verified as
2026.9.23.0, commit71c91d059f..., sha25652E9DF49.... Across the Store leg onlyopenclaw-tray.log,Logs\diagnostics.jsonl, andext4.vhdxchanged; nothing was added or removed.gateways.json,settings.json, and the device key are byte-identical from the pre-uninstall baseline through to a running Store app.User answers No to the local gateway prompt during Inno uninstall:
The current-head Store build lists the preserved gateway:
Final verdict, all checks pass:
Limits. Credentials are synthetic and
gateway.example.testdoes not resolve, so the Connection screenshot showsAuthentication failedby design. This proves preserved state is read and adopted; it does not claim a live gateway handshake. Volatile files are reported by name rather than excluded.Real gateway migration on a developer host
Run on a developer machine at head
71c91d05against a real gateway: paired via device token, running for 3 days, with real credentials, device keys, and a registered WSL distro. This is the live counterpart to the synthetic VM run above, where the preserved state was a fixture.completed.dpapi694 b,intent.dpapi6,550 bgateways.json130374774Bbefore and aftersettings.json33AEB8D49Dbefore and afterA156E37B91OpenClawGateway-Devstill registeredcompleted.dpapiandintent.dpapiremovedThe missing gateway prompt is the designed path rather than luck.
CheckCompletedStoreMigrationreturnsMigrationResult = 10, which setsLocalGatewayCleanupRequested := Falseand exits before the prompt atinstaller.iss:354.The Store app reports
Install type: Packaged (MSIX)and version2026.9.23.0, so the connected session is the Store package and not a surviving Inno process.Consent dialog before any change
Inno uninstall completes with no local gateway prompt
Store build connected using the preserved device token
Packaged identity of the connected app
Limits and disclosures.
Test-InnoMigration.ps1. The path proven here is receipt-present (decode, then exit 10), which the fix leaves unchanged.settings.jsonis byte identical and containsEnableNodeMode: False, a value that was set about six minutes before migration began. The individual capability toggles (NodeCanvasEnabled,NodeScreenEnabled,NodeSystemRunEnabled) all carried over asTrue.OpenClawGateway-DevunderOpenClawTray-Dev. The production uninstaller targetsOpenClawGateway, so managed-distro ownership transfer is still not proven. This is a handoff and adoption proof, not an ownership-transfer proof.Not verified / blocked
71c91d05(see above). Remaining gaps in that area: the installer under test is unsigned, and no logoff/reboot startup proof is claimed.AutoStart=true, startup-platform refusal, real multi-user access-denied classification, and the complete interruption/failure matrix.openclaw:acceptance, shipping accessibility, non-English visual layout, and interactive Windows Settings uninstall. Fixture removal was silent Inno uninstall.InitializeUninstallis Inno Pascal and no executing unit test runs it. Its runtime behavior is now exercised by the eight-case matrix and the two-arm authority proof above rather than by unit tests, so regressions in the Pascal path depend on that VM harness, which is session-local and not a committed repository entry point.Security Impact
Compatibility and Migration
--uninstall --confirm-destructiveremains deliberate full removal and is never invoked by migration.docs/uninstall-portable.md.Review Conversations
Both reported code findings are addressed by
e547733cand46fd8b6e: cross-session source exclusion and inaccurate consent copy. Maintainer acceptance of always-on Inno safeguards and release compatibility proof remain distinct rollout requirements. This PR is no longer a draft and is open for team review; maintainer edits remain enabled.