Repository navigation
feat(migration): guide users through the Store migration - #2
Closed
natalie-aguinaldo wants to merge 29 commits into
Closed
natalie-aguinaldo wants to merge 29 commits into
natalie-aguinaldo wants to merge 29 commits into
Conversation
Add the migration window and serial workflow, protected explicit consent, Inno Store handoff, graceful shutdown with manual Retry, Installed apps guidance, accessibility, and localization. Reuse the migration foundation and keep production migration disabled. Validation: full build; Shared 4039 passed, 33 skipped; Tray 3065 passed; Connection 1098 passed, 1 skipped; focused UI 16 passed. Test-signed x64 VM manual-fallback and graceful-shutdown migration journeys passed. Non-Codex review completed with no remaining actionable findings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fff6a10-5ecd-40d8-89fe-9a053aba38a5
Reuse the wizard mascot, Mica backdrop, centered heading, themed card, and persistent footer actions while preserving the independent migration workflow and disabled production gate. Cover light and dark themes, compact layouts, enlarged text, and accessible actions in mounted UI tests. Validation: full build passed; Shared 4039 passed, 33 skipped; Tray 3065 passed; focused migration UI 12 passed. Inspected current mounted XAML screenshots; no renewed VM or compositor-Mica proof is claimed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Integrate source lifetime safeguards, keep consent compatible with running Inno, centralize release build policy, and pin the selected 2026.9.5.0 source floor. Production remains disabled pending real release acceptance. Validation: full build passed; Shared4039 passed/33 skipped; Tray3108 passed; Connection1123 passed/1 skipped; mounted migration UI12 passed before the floor-only change. Configured x64 and ARM64 artifacts build; native ARM64 and official Store acceptance remain blocked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Merge PR1 at22db8274 including current main. Preserve the dedicated PR2 migration workflow and direct button labels, combine migration/chat locale resources and architecture owners, and retain production XAML metadata delegation in the UI test host. Validation: full build passed; focused migration93 passed; Shared4087 passed/33 skipped; Tray3160 passed; Connection1123 passed/1 skipped; mounted migration/native-chat UI97 passed/1 skipped. Eleven current-source migration screenshots captured. Non-Codex merge-resolution review found no actionable issues. Production remains disabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
…t copy Align the Settings promotion card with the other cards on the page and cut the consent dialog from four paragraphs to a short question. The card dropped its accent icon tile, which no other card on the page has and which pushed the text column out of alignment with the rows below. The badge now leads the title instead of stacking above it, so a long localized title cannot clip it. The action moves inline to the right of the text at 720px and wider, and reflows below the text when narrower. The consent dialog now asks a question and discloses only what the user is actually agreeing to. It previously restated the uninstall and Retry steps, which Migration2_AwaitingRemoval already delivers at the point where they apply. This reverses the disclosure contract added in f0017f4. That test required the long-form copy verbatim in every locale, including the 30-day term and the button labels. The rewritten test keeps the substantive disclosures and drops the ones that only repeated later stages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
VM proof showed the action stacked below the card text at 1085.6 and 1517.6 DIPs, where it should have sat inline. The VisualStateManager group was declared on a Grid nested inside the card, and WinUI only evaluates state triggers declared on the page root, so the AdaptiveTrigger never activated at any window width. Moving the group to the root fixed it, and proof then showed the narrow state is unreachable: HubWindow sets MinWidth=1000, so a 720 trigger is always satisfied and the stacked fallback can never render. The states, trigger, and setters are removed and the card is a single row, matching every other settings row. The action cannot starve the text column because it is capped at MaxWidth=320 and wraps its own label, leaving roughly 650 DIPs for text at the narrowest possible window. The contract test previously asserted only that the trigger existed somewhere in the card, which passed while the feature was dead. It now asserts that no AdaptiveTrigger or VisualStateManager group exists in the file at all, and names the window minimum as the reason. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
…ackage Migration.Build.props now defaults MigrationProductionEnabled to true, so the coordinated 2026.9.5 Inno and Store artifacts carry both migration surfaces. Debug, Dev, and preview builds stay excluded by the existing conditions. Neither existing MSIX artifact can exercise migration: the Store package is an unsigned submission asset, and the Dev package is refused by the Store guard because its identity is not the production one. CI now also publishes openclaw-msix-dev-migration-test-<arch>, the production-identity package test-signed with a disposable certificate whose subject matches the production publisher. The exporter refuses an already-signed package and refuses any build without migration compiled in, so the artifact cannot ship silently disabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
…ock failure InnoMigrationStartupGuard ran unconditionally in every build, including non-migration ones, and treated a failed MigrationOperationLock acquisition as fatal. Both models in the dual-model review flagged it independently. - Gate the guard body behind !INNO_MIGRATION_PREVIEW && !PRODUCTION_MIGRATION, mirroring StoreMigrationStartupGuard. The lease field and call-site ordering stay intact so the contract test's ordering assertions still hold. - Log and continue when the lease cannot be acquired instead of blocking launch. InnoSourceActivityVerifier scans the source image by exact path, including instances that never take the runtime file lock, so the Store-side interlock is preserved without the lease. - Add FormatException to the receipt-rejected catch filter. BinaryReader .ReadString throws it on a malformed 7-bit length prefix, which previously escaped as an unhandled exception. Validation: build.ps1 clean; Shared 4087 passed; Tray 3185 passed; Connection 1123 passed; InnoMigrationContractTests 24 passed. Both compile paths (gated-off and enabled) build with 0 errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
…om review Mechanical follow-ups from the dual-model PR review. No workflow states or user-facing decisions change; each item makes an existing path handle reality it already claimed to handle. Engine: - Add FormatException to the adoption, completion, and finalization catch filters. BinaryReader.ReadString throws it on a malformed 7-bit length prefix, verified with a standalone probe, so a corrupt record previously escaped as an unhandled exception instead of a ValidationFailed result. Add CryptographicException to the adoption filter for the same reason. - Report a reparse point above the record directory as Unavailable rather than Invalid. Invalid routes to RecoveryRequired, which tells the user to check logs and preserve data; recovery cannot repair a junction. Adds MigrationRecordCodec.HasReparsePointAncestor so only the startup reader changes classification. RejectReparsePoints and every other caller, including InnoInstallationDetector's deliberate Unsupported result, keep their existing behavior. UI: - Clear the migration window error bar when the stage changes. An "Installed apps" launch failure previously stayed on screen through every later stage. Scripts and CI: - Reject gateway record ids that are not a single path segment before Remove-Item -Recurse -Force on the derived identity directory. - Prefer the host architecture when resolving SignTool and order candidates by parsed SDK version instead of lexicographically, so 10.0.26100 beats 10.0.9xxxx and an ARM64 runner does not sign through emulation. - Verify the disposable signing key is gone instead of discarding the removal error. The check runs after the finally block so it cannot mask an in-flight failure. - Assert SignTool's real arguments in the harness; the fake previously accepted any arguments beginning with "sign". - Set retention-days to 30 on the migration test artifact to match the disposable certificate lifetime. The package is not timestamped, so a longer retention would only offer an artifact that can no longer install. Tests: new ReparsePointAncestor_IsUnavailableNotRecovery. The FormatException filters are defensive and not reachable through the public API, which only encodes well-formed records. Validation: build.ps1 clean; Shared 4087 passed; Tray 3185 passed; Connection 1124 passed; test-migration-test-msix.ps1 passed; test-ci-workflow-contract.ps1 passed; MigrationRecordCodec.cs still compiles under csc.exe /langversion:5 for the Inno uninstall path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Store-side migration treated "the window closed without finishing" as a reason to withhold the app. Declining the offer, an unsupported source install, a failed inspection, or a transient error all left the user with an app that would not start, and relaunching only re-showed the same window. Refusing to launch repairs none of those states. Separate the two concepts that were conflated in AllowsNormalStartup: - AllowsNormalStartup still answers "can we skip the migration window". - The new BlocksStartup answers "would starting normally abandon data", and is true only for FinalizationRequired and AwaitingInnoRemoval, the two states that hold a completion receipt. StoreMigrationWorkflow tracks that as _holdsCompletedHandoff, seeded from the admission and set again when CompleteAsync writes a receipt mid-session. It is deliberately not derived from the current stage: finalization can fail into InspectionFailed while a receipt still exists, and a stage mapping would then let the app start and abandon it. Also fix a launch-blocking defect found while reading the admission path. InnoInstallationDetector.Detect parsed the registered version before the payload-file check, then discarded it on the Unsupported return, so a pre-migration Inno install could never reach UpdateInno. Carry RegisteredVersion on the payload-missing path and return UpdateInno when it is below the pinned floor, so those users are told to update instead of being told migration is unavailable. Update RELEASING.md to describe the new dismissal and blocking semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Two release-path gaps found in review. The migration switch was not really a switch. Export-MigrationTestMsix.ps1 throws when the packaged assembly carries no MigrationMinimumSourceVersion, and that step runs unconditionally in build-msix, which release depends on through build-msix-bundle and ci-gate. Setting MigrationProductionEnabled to false therefore made every release impossible rather than producing a release without migration. Resolve the effective switch from Migration.Build.props (honoring an environment override) and gate both the export and its upload on it. Both steps need the same condition because the upload sets if-no-files-found: error. RELEASING.md also promised more than the switch delivers. It is a pre-publication gate: once a Store package carrying PRODUCTION_MIGRATION ships, the floor and the flow are compiled into that package and a later Inno build cannot disable migration for those users. Say so. installer.iss compared GetFileAttributesW's failure sentinel against an unsigned $FFFFFFFF literal whose type Pascal Script resolves inconsistently. A wrong comparison here is silent and fail-open: the reparse-point bit would be read out of a failure value. Declare the import as Integer and compare against -1 instead. The bit pattern is unchanged and the $400 test is unaffected. installer.iss was only compiled in the tag-only release job, so that class of error would have surfaced at release time. Add Test-InstallerScriptCompiles.ps1 and run it in fast-validation. It compiles the x64, arm64, and DevBuild variants against a stub payload, covering the migration lock code that lives inside #ifndef DevBuild. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
…ate name Dual-model review found that BlocksStartup whitelisted state names, so a completion receipt could coexist with a state that reads as informational and the app would start anyway, abandoning moved data. Reported reachable paths: an Inno self-update leaving the recorded source version mismatched, an unsupported/drifted source, an architecture mismatch, and an undecodable or locked receipt. - Carry HoldsCompletionReceipt on StoreMigrationStartupDecision and derive BlocksStartup from it, so a receipt outranks every informational state. - Report CompletionPresent from the record reader by existence, so a receipt that cannot be decoded or is locked still protects the handoff. Invalid no longer conflates a corrupt receipt with a corrupt intent. - Treat closing as blocking until the first admission resolves; dismiss was reachable while the initial inspection was still in flight. - OR the workflow handoff flag instead of assigning it, so Retry re-inspecting into a transient failure cannot drop a receipt already observed this session. - Install Inno Setup in fast-validation. The ISCC gate added in a0c5050 ran with -RequireCompiler on an image that does not ship it, which would have failed every PR. Validation: build.ps1 5/5, docs 49, Shared 4087/0, Tray 3197/0, Connection 1150/0, UITests StoreMigrationWindowProof 13/0, ISCC 3 variants, test-ci-workflow-contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
…erify key removal on failure Two remaining review findings, both on the uninstall/export scripts. - Uninstall-LocalGateway.ps1 validated the gateway id as a single path segment but still called Remove-Item -Recurse on a directory that could itself be a junction. Windows PowerShell 5.1, which is what runs this during Inno uninstall, recurses through reparse points, so the delete would destroy the junction target. Reject reparse points there the way MigrationRecordCodec already does everywhere else. Anchor the id pattern with \A and \z, since .NET lets $ match before a trailing newline. - Export-MigrationTestMsix.ps1 verified that the disposable signing key was gone only after the try/finally, so the check never ran on the failure path, which is when a key is most likely to be left in Cert:\CurrentUser\My. Warn from the finally as well, and keep the hard failure on the success path. Validation: Tray 3198/0 (new CleanupScript_RefusesToDeleteThroughAReparsePoint), test-migration-test-msix, PowerShell parse check on both scripts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
MigrationPathIsOrdinary walked ancestors with ExtractFileDir until it reached a
fixed point. That terminates correctly on a local disk, where ExtractFileDir
returns 'C:\' for 'C:\', but a UNC path has no such fixed point:
ExtractFileDir('\\server\share') yields '\\server', which is not a filesystem
object. GetFileAttributesW then fails with a code that is neither
ERROR_FILE_NOT_FOUND nor ERROR_PATH_NOT_FOUND, the guard exits False, and
InitializeUninstall refuses to uninstall at all.
With AppData redirected to a share, that wedges the user both ways: the Inno
app cannot be removed, so migration stays in AwaitingInnoRemoval and the Store
app never starts.
Stop at the share root instead. Every component below it is still checked, so
this restores the local walk's semantics rather than relaxing the guard.
Validation: Tray 3199/0 (new Installer_StopsTheReparseWalkAtAUncShareRoot),
ISCC 3 variants.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
A second review round found that the receipt-based blocking introduced earlier only held where a receipt was successfully read. Every error path still fell through to "start normally", which is the data-divergence this feature exists to prevent. MigrationStartupRecordReader: File.Exists reports false for an inaccessible path, an invalid path, and a directory of the same name, so it cannot tell absence from failure and failed open on exactly the paths that matter. Probe attributes instead and treat only a definite not-found as proof that nothing moved. The rejected-path handler now probes too: refusing to read through a reparse point is not evidence that data stayed put. StoreMigrationStartupCoordinator: read records before the version/architecture policy check, so a misconfigured build cannot return a non-blocking decision while a receipt is on disk. A Completed status with no decoded record now recovers instead of throwing, because that throw escaped into the UI error boundary and was reported as a plain inspection failure, dropping the receipt the status proves exists. StoreMigrationWorkflow: an inspection that threw never observed a receipt, but the finally still marked admission resolved, so closing the window resumed startup. Probe the receipt directly on that path, and accept the caller's admission so a receipt seen by StoreMigrationStartupGuard is not discarded by a failing second pass. Uninstall-LocalGateway.ps1: the reparse guard checked only the identity directory. A junction at 'gateways' redirects the whole subtree while the leaf still looks ordinary, so Remove-Item -Recurse deleted the target. Walk every ancestor up to the data directory. Export-MigrationTestMsix.ps1: guard the residue probe inside the finally, which could itself throw under ErrorActionPreference Stop and mask the real failure. The new junction coverage executes the shipped guard text against real junctions; the previous source-order assertions passed while the bug was live. Verified failing against the leaf-only guard before restoring the fix. Validation: build.ps1 5/5, docs 49, Shared 4087/0, Tray 3205/0, Connection 1156/0, UITests StoreMigrationWindowProof 13/0, ISCC 3 variants, PowerShell parse checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
PR1's branch gained ten commits while this branch was in review. Resolve the six conflicts in favour of whichever side made the later, narrower decision, and port PR1's two behavioural fixes into this branch's architecture rather than reverting the architecture to match them. Resolutions: - StoreMigrationStartupGuard.cs keeps the windowed flow. PR1's durable autostart refusal is ported instead: StoreMigrationAutoStartApplier now rethrows StoreMigrationAutoStartRefusedException for durable refusals, and the workflow maps StartupPreferenceRefused to a new StartupRefused stage before the allows-normal-startup fold. StartupRefused reuses the existing Migration_StoreStartupRefused string and stays outside BlocksStartup. - MigrationOperationLock.cs restores PR1's inline DACL hardening and ScrubExistingEntries, which this branch's MigrationRecordStorage extraction had silently weakened. - InnoMigrationStartupGuard.cs takes PR1's narrower lock handling: only a contended lock blocks Inno startup, an unreadable one degrades to the receipt check. Both branches fixed the same bricking bug; PR1's carve-out keeps the in-progress interlock this branch had dropped. - StoreMigrationFinalizationCoordinator.AllowsNormalStartup keeps PR1's broadened set, so a transient StartupPreferenceFailed no longer strands the user on a failure card. The workflow theory follows that semantics and a new behavioural test covers the durable refusal. - MigrationRecordTests.cs adopts PR1's exit-code semantics (a receipt that exists but does not validate is exit 2, never 0) alongside this branch's new consent kind. - InnoMigrationContractTests.cs unions both suites. PR1's StorePreviewChoices_UseNativeYesNoWithoutActionLegends is deliberately dropped because it asserts the MessageBox flow this branch deleted, and PR1's Task.Run(...).ConfigureAwait(false) assertion no longer applies to an applier that plain-awaits. - docs/RELEASING.md keeps this branch's full shipping journey. Validation: build.ps1 5/5, docs 49, Shared 4087/0, Tray 3227/0, Connection 1201/0, UITests 13/0, installer.iss compiles for x64, arm64 and DevBuild. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
The StartupRefused stage arrived with the PR 1 merge and is the only visible window state with no capture. It renders new user-facing copy, hides Primary because there is nothing to retry, and focuses Dismiss. Mount the production window with in-memory operations, drive finalization to StartupPreferenceRefused, and assert the refusal string, the collapsed Primary, the polite live region, and that dismissing resumes startup because no receipt blocks it. Capture the rendered state under OPENCLAW_VISUAL_TEST. Validation: build.ps1 5/5, docs 49, Shared 4087/0, Tray 3227/0, UITests 14/0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
10 tasks done
Eight resource values were XML-escaped one time too many, so users saw the raw escape sequence instead of the intended character. The MSIX uninstall warning in Settings > Local Gateway rendered "Windows Settings → Apps", losing the navigation-path arrow. The InfoBar carries an x:Uid, so the .resw value wins at runtime and the correct XAML attribute literal is only a design-time fallback; the string was escaped again when it was lifted into .resw. fr-fr, nl-nl, zh-cn and zh-tw were a step worse at "&#x2192;" because the translation pass escaped it once more. The same re-escaping damaged seven ampersand keys in fr-fr and nl-nl, where "Save & connect" and "Read & write" rendered as "Enregistrer & connecter" and "Lezen & schrijven". Store the literal characters instead of numeric references so another escaping pass cannot reintroduce this, matching the roughly eighteen arrows per locale that are already stored literally. Add Resources_DoNotContainOverEscapedXmlEntities, which decodes every .resw value and fails when an entity survives decoding, since a well-formed resource can never produce one. Verified it is not vacuous by reinjecting both damage families and confirming it reports the offending locale, key and token. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
A user who added a gateway but never finished pairing could not migrate. Completion resolved an operator credential and refused when none existed, and the resulting card told them to "restore the gateway configuration in the previous app" even though nothing was damaged. Retry could not clear it, and removing the gateway record landed on the same stage through NoActiveGateway, so there was no way forward. Reproduced in a VM end to end. The check protected nothing. Migration never moves gateway state: both installs read the same roaming directory, so a credential resolves exactly the same before and after. The resolved credential was used only for a null check; it is never written to the receipt and never copied. The damaged case it appeared to cover is handled earlier and more strictly. MigrationInventory.CaptureIdentities rejects any unreadable or corrupt identity, and capture runs during preparation and again at completion before the fingerprint compare. A test that damages key material proves preparation throws, so completion was only ever reachable when no credential was configured, which is the case that should be allowed. Note the old gate was weaker than it looked: a corrupt identity with any fallback token passed it. Remove the check and the now-unreachable NoActiveGateway and CredentialUnavailable completion states, the unreachable StoreMigrationStage.CredentialUnavailable and its window arm, and the orphaned Migration2_CredentialUnavailable and Migration_StoreCredentialUnavailable strings across all six locales. Drop the ICredentialResolver dependency so the coordinator structurally cannot resolve credentials, and invert the contract assertion to guard that. Add coverage for completing without a credential, for an unpaired gateway, and for damage introduced both before preparation and between preparation and completion, so the earlier fail-closed path stays proven. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
…re-migration' into user/natalie-aguinaldo/inno-to-store-shipping
Adversarial dual-model review found that RecoveryRequired could strand a user permanently: the workflow refused to re-run from Recovery, the window collapsed the primary action there, BlocksStartup stayed true while a receipt was held, and the guidance text forbade the only remedy that works. - Recovery is retryable again, so removing the previous app is noticed. - Recovery shows the Installed apps shortcut alongside Retry. - StoreMigrationRecoveryDiscard removes migration records that cannot be decoded at all, under prepare.lock and with reparse rejection, and re-reads status under the lock so a decodable record is never discarded. Uninstall preserves data while the Store package is registered, so this cannot lose user state. - The uninstall cleanup reparse guard now walks descendants, because PowerShell 5.1 Remove-Item -Recurse follows a junction planted inside the identity directory. - Copy no longer claims gateway-credential validation or a paused startup that is not held. Validation: build.ps1; Shared 4087; Tray 3243; Connection 1208; Tray UI StoreMigrationWindowProofTests 15. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
The re-review of f9d5d66 found the recovery escape hatch still had three ways to leave a user stuck. Records were classified through the aggregate startup status, which stops at the first unreadable file. A corrupt receipt therefore hid a perfectly good intent behind Invalid, and discarding took both. Classification is now per file, so a corrupt receipt costs only the receipt. A decodable intent left behind after the previous app is uninstalled could never be acted on again, but the old gate withheld discard because the record still decoded. Recovery then nagged forever with no way out. Discard now also covers a leftover consent or intent once the source is gone and no readable receipt remains. The previous app holds the migration lease for its entire lifetime, which makes a contended discard the most likely first outcome. That result was only logged, so the button looked dead. The window now reports it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
…tion The third review pass found the discard could still strand a user. Opening prepare.lock with FileMode.Open meant a missing lease file threw before any planning ran, and that throw was caught as "nothing to discard". Records stayed on disk while the window reported success, so recovery returned on the next pass with nothing to show for it. The lease is now opened with OpenOrCreate, and only a missing directory counts as nothing to discard. Classification had the repository's exception convention backwards. Operational failures were rethrown and anything else nominated a file for deletion, so a record that failed to parse was reported as lock contention while an unexpected error made a file deletable. Reading and decoding are now separate: only the same allowlist the startup reader uses marks a record corrupt, and everything else propagates. A rejected path shape travels as MigrationPathRejectedException so it is no longer reported as the previous app holding the lease. A contended discard also left its message on screen for every later stage, and overwrote the launch failure. A new pass now clears it, and the window shows it only in recovery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
…ntention BinaryReader reports a malformed string length as a plain IOException. The decode allowlist did not include it, so a record that decrypted but did not parse escaped classification as an operational failure. CanDiscard then returned false and hid the button while startup stayed blocked, which is the lockout this class exists to remove. The decode block has no file IO left in it, so IOException there can only mean a malformed record. The damaged-record test only cut the protected bytes in half, which fails in DPAPI and never reached the parser, so it passed either way. It is now joined by a record that decrypts cleanly and carries a negative string length. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
…on failure The discard offer only exists on the recovery screen. The startup reader decoded records without separating the file read from the parse, so a record that decrypted but carried a malformed string length surfaced as a plain IOException and was reported as an inspection failure. A receipt in that state blocked startup from a screen with no way out, which is the lockout the discard was added to remove. Reading the bytes and parsing them are now separate, and only the parse treats an IOException as corruption, the same split InnoMigrationConsentStore already uses. A path rejection still propagates ahead of it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
…re-migration' into user/natalie-aguinaldo/inno-to-store-shipping
Granting consent rewrote the directory owner and DACL on every call once the directory existed. That rewrite needs WRITE_DAC and WRITE_OWNER, which a roaming or administratively hardened profile can withhold from an ordinary launch, and a refusal is not an invalid record: it escapes the consent store's allowlist and surfaces as a generic inspection failure, blocking a migration whose state was already correct. Reuse the drift check the operation lock already applies for the same reason instead of duplicating it, so an already-hardened directory is left alone while real drift is still corrected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
11 of 16 tasks
Owner
Author
|
Closing in favor of openclaw#1519 now that PR 1 merged |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related: openclaw#1374
Stacked PR 2 of 2: complete migration experience and production enablement. Depends on openclaw#1461 (feat(migration): add gated Inno-to-Store migration preview). This draft is intentionally opened in the contributor fork against
user/natalie-aguinaldo/inno-to-store-migration, so the review diff contains PR 2 rather than the whole foundation.Shipping goal: resolve the remaining scope of openclaw#1374 in PR 2, including production enablement and release acceptance. There is no planned PR 3. The migration-capable Inno app and enabled Store app may ship in one coordinated release; a separate preliminary Inno release is not required.
Current head:
5714c8aa. Production migration is enabled by default for Release non-Dev builds, so the coordinatedv2026.9.5Inno and Store artifacts carry both migration surfaces, and CI publishes a package developers can actually install to exercise them.Since this writeup was first drafted the branch has been through an independent adversarial review that found four fail-open defects, and PR 1's branch has been merged in. See Correctness hardening and Merging PR 1.
Enabling the switch is a merge-time decision; shipping it is not. Real Store, ARM64, and live-gateway acceptance are tag-time gates listed under Outstanding acceptance. This draft is not rollout approval, and merging it within the fork does not ship upstream or close the upstream issue.
GitHub does not support Allow edits from maintainers on this same-repository stacked draft (the API rejects it as cross-repository-only). Enable it when publishing a cross-repository PR upstream; this draft does not grant upstream maintainers write access to the fork.
What Problem This Solves
The migration foundation needs a clear, accessible handoff that obtains consent, stops the previous app safely, and guides users through removing it before the Store app becomes active.
User Impact
Explicit Debug previews and opted-in production-identity Release builds provide a wizard-style migration window with Migrate / Not now, progress, manual close / Retry recovery, and Open Installed apps. Supported settings, gateway data, identities, and models remain in place. Users must uninstall the previous Inno app themselves; migration does not force-kill or automatically uninstall it.
Release non-Dev builds at the current head enable this flow by default. The shared build policy pins the source floor and product ID and now sets
MigrationProductionEnabled=true; Debug and Dev builds still cannot inherit production activation. The confirmed Store listing is OpenClaw, product ID9NFPR3BGDRR5. Actual Store-distributed handoff and release acceptance are still pending, and are now tag-time gates rather than merge-time gates.Enablement and the developer migration test package
Neither existing MSIX artifact can exercise migration. The unsigned Store package is a submission asset and cannot be installed, and the Dev package is refused by
StoreMigrationStartupGuardbecause its identity is not the production one. Enabling the flag alone would therefore have produced artifacts nobody could test.CI now also publishes
openclaw-msix-dev-migration-test-<arch>fromscripts\Export-MigrationTestMsix.ps1: the production-identity Store package, test-signed with a disposable certificate whose subject matches the production publisher. "Dev" in that name means for developers; the package identity is production. ItsINSTALL.txtstates plainly that it shares a package family name with the Store release and is therefore not side-by-side, that migration runs against real data directories because the guard rejects path redirection, and that it will adopt and uninstall an Inno installation. The exporter refuses an already-signed package, and refuses any package whose assembly lacks the migration metadata, so the artifact cannot silently ship disabled. Measured export results are under Real behavior proof.Because every unhappy Store-side path stops launch rather than degrading,
docs/RELEASING.mdnow also requires confirming before the tag that the released Inno installer registersDisplayVersion2026.9.5andDisplayNameOpenClaw Companion version 2026.9.5. A prerelease suffix or mismatched name is rejected byInnoInstallationDetectoras an unsupported installation, which blocks startup for that user. The already-publishedv2026.9.5-alpha.*builds carry a prerelease suffix and fall into that category.Why This Change Was Made
Ownership:
StoreMigrationStartupGuardpreviously hosted the native dialog loop;StoreMigrationWorkflownow owns serial interaction,StoreMigrationOperationsadapts existing migration coordinators, andStoreMigrationWindowapplies the UI. The startup guard retains the compile-time gate and pre-services window lifetime.InnoMigrationHandoffowns the gated consent/listing/shutdown integration.Stack status and merge resolution
PR 1 at
22db82746143542f3d69f69813f6884b43a3b780was integrated first, and PR 1's ten subsequent commits were merged in atf284c258(see Merging PR 1). Published merge3fd9358d406359b18731e1d7ebea648c4437c76cresolved the foundation/main conflicts; this follow-up changes presentation, localization, documentation, and tests only. The serial workflow, migration coordinators, startup gate, shutdown protocol, and fixed source floor are unchanged.PR 2 retains its WinUI workflow and directly labeled Migrate / Not now / Retry buttons, not PR 1's obsolete native dialog loop. All six locales retain the legacy templates and concise composed PR 2 disclosures. Production/native XAML metadata delegation remains intact.
Selected release plan
The selected target is the next Inno release after the current published
v2026.9.4:v2026.9.5, withMigrationMinimumSourceVersion=2026.9.5.0pinned inMigration.Build.props. Both PR 1's safeguards and PR 2's complete migration flow must be included in that coordinated release. No separate preparatory Inno release is required.Users on older Inno versions must update to
v2026.9.5or newer before migrating to Store. The minimum stays fixed in subsequent releases; it must not track each later app version or the Store package version.This resolves release-version selection and configuration pinning, not artifact verification or production activation. If the next release receives a different version or does not contain both PRs, revise the pinned minimum before shipping.
Remaining work for the complete shipping change
Integration, build policy, enablement, product-ID wiring, release selection and pinning, the test-signed x64 VM journey, and the documentation updates are complete and described above. Everything still outstanding is listed once under Outstanding acceptance, and all of it gates the tag rather than the merge.
The same release can introduce the full migration flow and establish the minimum supported Inno version. The minimum is a compatibility requirement for the installed source uninstaller, not a requirement for a separate earlier release.
Evidence
Head:
5714c8aa, with PR 122db8274and PR 1's current branch tip as ancestors.Everything in the screenshot sections below still describes the shipped flow; the commits since
72e69409change error-path behavior, uninstall safety, CI gating, and tests, not the workflow the screenshots show.f0017f4dused its exact production-identity Release x64 Inno fixture and test-signed MSIX. All 980 MSIX and 972 Inno payload entries matched the built artifacts.Correctness hardening after adversarial review
The migration flow was put through an independent read-only review after the UI work settled. It refuted most
of my own earlier self-assessment and found four HIGH-severity defects that shared one root cause: the
completion receipt was authoritative only where a receipt was successfully read, so every error path fell
through to "start normally." On a machine that had already migrated, that means the old Inno app launches again
against data the Store app now owns.
File.Existsfails open. It returnsfalsefor a directory of the same name, a missing parent, or invalid characters, and never throws, so thecatch { return true; }fallback was dead code and the doc comment promised the opposite of the behavior.MigrationStartupRecordReader.CompletionFileExistsnow usesFile.GetAttributes, which throws distinguishably. OnlyFileNotFoundExceptionandDirectoryNotFoundExceptionreturnfalse; everything else returnstrue.gatewaysredirects the whole subtree whilegateways\<id>still reports as an ordinary directory, so a recursive delete followed it. The reviewer reproduced real data loss under PowerShell 5.1.Uninstall-LocalGateway.ps1now walks every ancestor up to$DataDirbefore deleting, and stops at a UNC share root so folder-redirected AppData cannot block uninstall.StoreMigrationStartupGuardinspected twice and discarded the first result, so a successful first pass followed by a failing second pass lost the receipt._holdsCompletedHandofffrom it.catchin the workflow could not tell "no receipt" from "could not tell," andStoreMigrationStartupCoordinatorthrewInvalidOperationExceptionwhen a completed migration had no receipt.IStoreMigrationOperationsgainedHoldsCompletionReceipt(), deliberately independent ofInspect(), with a non-throwing variant that fails safe totrue. The coordinator returns aRecoveryRequireddecision instead of throwing.Source-text contract tests gave false assurance here. The reparse-ordering assertions passed while the bug
was live, because they asserted that markup existed, not that it ran. They were replaced with a behavioral
test that extracts the shipped guard text from
Uninstall-LocalGateway.ps1, wraps it in a harness, and runs itunder
powershell.exeagainst a real junction. It was confirmed to fail against the pre-fix code and passafter, and the junction case it catches is exactly the one the leaf-only guard missed.
Two further release-path gaps were closed in the same round:
Export-MigrationTestMsix.ps1throws when the packagedassembly carries no
MigrationMinimumSourceVersion, and that step ran unconditionally inbuild-msix, whichreleasedepends on. SettingMigrationProductionEnabled=falsetherefore made every release impossiblerather than producing a release without migration. CI now resolves the effective switch from
Migration.Build.props(honoring an environment override) and gates both the export and its upload on it.installer.isswas only compiled for real in the tag-only release job, so a Pascal Script error wouldhave been found at release time. CI now runs
scripts/Test-InstallerScriptCompiles.ps1 -RequireCompileronevery PR.
Merging PR 1 into this branch
PR 1's branch gained ten commits while this one was in review. They were merged at
f284c258. Six filesconflicted. Each was resolved in favor of whichever side made the later, narrower decision, and PR 1's
behavioral fixes were ported into this branch's architecture rather than reverting the architecture to match
them.
StoreMigrationStartupGuard.csStoreMigrationAutoStartRefusedException, and the workflow mapsStartupPreferenceRefusedto a newStartupRefusedstage before the allows-normal-startup fold. It reuses the existingMigration_StoreStartupRefusedstring and stays outsideBlocksStartup, so the user is told that only Windows can re-enable the startup task without being trapped.MigrationOperationLock.csScrubExistingEntries, which this branch'sMigrationRecordStorageextraction had silently weakened.InnoMigrationStartupGuard.csStoreMigrationFinalizationCoordinator.csAllowsNormalStartup, so a transientStartupPreferenceFailedno longer strands the user on a failure card. Only a durable refusal notifies.MigrationRecordTests.csconsentkind.InnoMigrationContractTests.csStorePreviewChoices_UseNativeYesNoWithoutActionLegendswas deliberately dropped because it asserts theMessageBoxWflow this branch deleted, and itsTask.Run(...).ConfigureAwait(false)assertion no longer applies to an applier that plain-awaits.docs/RELEASING.mdThe merge surfaced four test failures that were genuine semantic collisions rather than noise, all from PR 1's
broadened
AllowsNormalStartupand this branch's newStartupRefusedstage. They were resolved by followingPR 1's semantics and adding behavioral coverage for the durable-refusal path.
Change Type
Scope
winnodeRequired proof pools
windows-winui-interactive: migration and Inno consent UX, focus/accessibility, localization, and activation behavior.windows-clean-installer-upgrade: exact supported Inno to Store package handoff, protected uninstall, and finalization.windows-11-arm64: architecture-matched ARM64 package/runtime acceptance.windows-wsl-gateway-e2e: existing managed WSL gateway preservation and continued management after handoff.These declarations request proof; they do not claim the complete pools passed.
Validation
Commands ran in the isolated PR 2 worktree with
OPENCLAW_REPO_ROOTset to that worktree. UI and Tray checks used isolatedOPENCLAW_TRAY_DATA_DIRdirectories..\build.ps1dotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csproj --no-restoredotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csproj --no-restoredotnet test .\tests\OpenClaw.Connection.Tests\OpenClaw.Connection.Tests.csproj --no-restore.\scripts\Test-InstallerScriptCompiles.ps1Migration2LocalizationTestsandInnoMigrationContractTests0651b419MigrationBuildConfigurationTests.\scripts\validate-docs.ps1CleanupScript_DoesNotDeleteThroughAJunction.\scripts\test-ci-workflow-contract.ps1.\scripts\test-migration-test-msix.ps1Migration2LocalizationTests,InnoMigrationContractTests, andStoreMigrationWorkflowTestsf0017f4dThe fourteenth test is new: it mounts the production window, drives finalization to
StartupPreferenceRefused, and asserts the refusal copy, the collapsed Primary button, the polite live region, and that dismissing resumes startup because no receipt blocks it.Mounted tests cover light/dark rendering, native small/large window icons, native button styles and spacing, stage headings, safety notices, and compact scrolling without hiding footer actions. The compact test enlarges heading/status fonts; it is not OS-wide text-scaling proof.
Historical regression coverage at the preceding published merge
3fd9358d: Connection 1,123 passed / 1 skipped; combined migration/native-chat/markdown/tool UI 97 passed / 1 skipped. Those broader suites were not rerun for this presentation-only follow-up. The prior UI skip wasReactorChatLayoutProofTests.ReasoningPicker_PointerReleaseCommitsButCaptureLossDoesNot.Real behavior proof
Enablement and the migration test package (current head)
The flag flip was verified against a real Release binary rather than only the MSBuild probe. A plain
dotnet build -c Release -r win-x64of the tray project produced an assembly carrying the pinned policy:scripts\Build-StoreMsix.ps1 -Architecture x64then produced a real unsigned Store package (Identity: OpenClawFoundation.OpenClaw 2026.9.5.0 x64), andscripts\Export-MigrationTestMsix.ps1was run against it end to end with the real Windows SDKSignTool.exe:OpenClaw-MigrationTest-x64.msix(157,079,494 bytes),OpenClaw-MigrationTest.cer,msix-metadata.json,INSTALL.txtOpenClawFoundation.OpenClaw,CN=4BA40A7A-B719-4C40-BF91-84AF4F1136FC,2026.9.5.0CN=4BA40A7A-B719-4C40-BF91-84AF4F1136FC(matches the manifest publisher)signing=migration-test-only,migrationEnabled=True,sideBySideWithStore=FalseCert:\CurrentUser\Myafter the runGet-AuthenticodeSignaturereportsUnknownErroron the signing machine because that disposable certificate is deliberately not trusted there; trusting it is step 3 of the generatedINSTALL.txt. The exporter verifies the signer thumbprint rather than machine trust, so it does not modify machine trust in CI.Not verified / blocked: this package was not installed or launched. Installing it replaces the production package family, so it belongs on a disposable VM, and the Store-side migration UI proof below is still the
f0017f4drun.Store migration entry point on the Inno Settings page (current head)
This is the complete path a user takes, from the installed Inno app to the Store app finishing the move. Steps 1 and 2 are current-head proof. Steps 4 through 7 reuse the
f0017f4dmanual VM run, because the Store listing is not live, so a Store-distributed install cannot be exercised yet.C4D2F8E4BD98B91E4C692A21DA2078301E43AE4333EA242E26D8C0A27D1A0A17, 972/972 payload hashes matched, measured at 985.6 client DIPs.In context with the settings rows below it, showing matching row height and text alignment:
Consent and handoff. Choosing the primary action records protected consent bound to the source version, user, architecture, canonical paths, and target package identity, then opens the Store listing.
InnoMigrationHandoff.GrantAndLaunchAsyncgrants before launching, so a user who abandons the Store has still granted. Choosing the neutral action records nothing.Install from Microsoft Store. The user installs the Store package themselves. Not exercised from a live listing; the
f0017f4drun used a locally test-signed MSIX.Store app requests shutdown. With a valid grant, the Store app asks the Inno app to exit through the current-user activation pipe. There is no force-kill. If the source does not exit, the user closes it manually and clicks Retry.
Layout bug found by VM proof, not by tests
The card action was specified to sit inline beside the text. VM capture showed it stacked below the text at 1085.6 and 1517.6 client DIPs. The
VisualStateManager.VisualStateGroupsblock was declared on a Grid nested inside the card, and WinUI only evaluates state triggers declared on the page root, so theAdaptiveTriggernever activated at any width. The full Tray suite passed throughout, because the contract test only asserted that the trigger existed somewhere in the card markup.Re-proofing after moving the group to the page root then showed the fallback was unreachable in principle:
HubWindowdeclaresMinWidth="1000", so a 720 trigger is permanently satisfied and the stacked state can never render for any user. Repeated attempts to size the window to 680 DIPs produced 985.6. The states, trigger, and setters were therefore removed and the card is a single row. The contract test now asserts that noAdaptiveTriggerorVisualStateManagergroup exists in the file, and names the window minimum as the reason.Deliberate reversal of the disclosure contract added in
f0017f4dMigration2LocalizationTests.InnoDialog_DisclosesSafetyBoundariesInEveryLocalewas added inf0017f4dto require the long-form consent copy verbatim in every locale, including the 30-day term and the neutral/primary button labels. That test has been rewritten to assert a reduced set of disclosures.This is an intentional policy change, not an accidental weakening. The dialog previously restated the uninstall and Retry steps, which
Migration2_AwaitingRemovalalready delivers at the moment they apply, so the same user read them twice, the first time out of context. The dialog now discloses what the user is agreeing to at that moment: the Store opens, the Store app can close this app and finish the move without asking again, and supported data stays in place. The 30-day expiry is still enforced byInnoMigrationConsentStore; it is no longer recited in the dialog.docs/RELEASING.mdacceptance disclosures for the Store preview window cover different keys and are unchanged.Current-head native icon diagnostics
StoreMigrationWindownow calls the existing WinUIExSetIcon("Assets\\openclaw.ico")helper, matching other application windows. Its custom XAML title-bar image alone did not populate native HWND icons.The new
Consent_AssignsNativeIconsForTaskbarAndWindowSwitchertest mounts the production window with in-memory migration operations and queriesWM_GETICONon its live HWND. It failed before the production fix because an icon handle was zero. Current-head output:Types 0 and 1 are the native small and large icons. This directly verifies native icon assignment. A developer-captured Explorer taskbar-hover screenshot from an isolated current-head VM preview confirms that the OpenClaw icon is visibly rendered. The restored VM originals were not modified for this follow-up.
Current-head taskbar preview
The screenshot was captured from the isolated 72e6940 preview with in-memory operations. It is visual icon proof, not another migration lifecycle run.
Startup-refusal window state (current head)
StartupRefusedarrived with the PR 1 merge and is the newest visible state. It is reached when migrationfinishes but Windows refuses to enable the startup task, which is durable and cannot be retried from inside
the app. The window therefore states what happened, tells the user where to change it, and offers only Close.
Captured at
5714c8aafrom the mounted production window with in-memory operations, the same harness used forthe native icon proof. It is visual proof of the rendered state, not a migration lifecycle run: no real
finalization, package, or startup-task change is exercised.
Manual VM migration at
f0017f4dOpenClaw-Migration-Auto-x64-a91896be, VM IDd0cd56dc-6a26-4493-8bc6-89f6cf2de790. No cloud lease ID or hosted run URL.f0017f4d035d70093f2ffe7e8b2fd3373c2bfaf1, tested before commit with no later source edits. Release x64, production package identity,DevBuild=false, migration enabled only by proof-time CLI override. Checked-in source floor2026.9.5.0was not overridden; the checked-in production default was still disabled at that time and is enabled at the current head.2026.9.5.0and locally test-signed MSIX2026.9.21.0. These are not official Store/release artifacts.gateway.example.testconfiguration and credential, with startup/node/MCP disabled. Connection/authentication errors in the normal-app screenshot are expected for that non-live gateway; they do not prove gateway connectivity.Test-InnoMigration.ps1returned exit code 10 for the canonical current-user source, confirming migration-preservation eligibility. The source app was no longer running.Store migration finalized:at 21:18:39.499, followed byApplication started (WinUI 3)at 21:18:40.755, after the pre-Retry verification gate. The same verified Store process continued. Consent, intent, and completion records were removed by finalization; settings, gateway, and generated identity hashes remained unchanged.Store-side consent at
f0017f4dSteps 5 through 7 of the walkthrough above are the rest of this run's screenshots; they are not repeated here. The one view the walkthrough does not cover is the Store-side consent window, whose Inno-side counterpart has since been rewritten.
All are developer-captured from the real VM and the exact
f0017f4dtested build, not mounted fake-operations captures. Every embedded URL returned HTTP 200 with image/png, and downloaded SHA-256 hashes match the inspected developer screenshots.Live migration at current head (
5714c8aa)A second manual VM run exercised the whole lifecycle against the current head instead of
f0017f4d, and used the checked-in production default rather than a proof-time CLI override.OC-AUTO-A91896, userMigrationTest. No cloud lease ID or hosted run URL.5714c8aa88a62fec8e339ba1215fda432eee6b58, clean tree (sourceTreeDirty: falseinmsix-metadata.json).OpenClawCompanion-Setup-x64.exe(ProductVersion2026.9.5, payload FileVersion2026.9.5.0) and locally test-signed MSIXOpenClawFoundation.OpenClaw 2026.9.5.0. Neither is an official Store or release artifact.InnoInstallationDetectorcondition (canonical per-user path,DisplayNamematchingDisplayVersion, publisher, exact quotedunins000.exeuninstall string, complete payload,releaseidentity, matching executable FileVersion), with no machine-wide registration present.Test-InnoMigration.ps1exit 0startup admission: ConsentRequired (receipt: False)preparation failed: Device identity is malformed or unreadable.preparation completed/completion recordedTest-InnoMigration.ps1all absentFinalizationRequired (receipt: True)/finalizedPreservation result. Comparing SHA-256 of every tracked file across the real uninstall,
gateways.json,gateways\<id>\device-key-ed25519.json, andsettings.jsonwere unchanged. Onlyconsent.dpapi,consent.lock, andintent.dpapidisappeared, which is finalization consuming its own records.The refusal below and the checker's exit-11 branch are proven here for the first time against a live migration rather than a mounted window. The card is deliberately generic.
What this run does not establish.
wss://gateway.example.test/with a shared token) and was never paired, so this is not live gateway continuity. The authentication error in the normal-startup capture is expected.gateways.jsonwas hand-authored mid-run after an operator seeding mistake overwrote the pre-existing fixture. The preservation assertion still holds, because it compares hashes across the real uninstall, but that file is not an organically produced artifact.%APPDATA%\OpenClawTraywas not pristine: a stale zero-byteprepare.lockand the generated identity directory predate this run. The source install was fresh and the baseline checker returned0.Unpaired-gateway block found by VM proof
The live run also exposed a user state with no way forward. A gateway that was added but never paired
has no device, shared, or bootstrap token, so completion refused and directed the user to restore a
configuration that was never damaged. Retrying could not clear it, and deleting the gateway record
reached the same stage through
NoActiveGateway.The refusal protected nothing. Migration leaves gateway state in place, both installs read the same
roaming directory, and the resolved credential was only null-checked, never written to the receipt.
Proving the damaged-credential half showed that branch was unreachable:
MigrationInventory.CaptureIdentitiesrejects an unreadable or corrupt identity during preparation, so completion was only ever reachable when no
credential was configured. The old gate was also weaker than it looked, because a corrupt identity carrying
any fallback token passed it.
The check was removed in
71509850, together with the unreachableNoActiveGatewayandCredentialUnavailablestates, theCredentialUnavailablestage and its localized strings, and theICredentialResolverdependency, so the coordinator structurally cannot resolve credentials. Coverage wasadded for completing without a credential, for an unpaired gateway, and for damage introduced both before
preparation and between preparation and completion.
Outstanding acceptance and disclosures
Tag-time gates. The switch is checked in as
trueso the coordinated release carries migration, but the tag must not be published until these pass against that tag's real artifacts. If acceptance fails, setMigrationProductionEnabledtofalseand retag rather than shipping an unverified launch gate.f0017f4dand5714c8aa).v2026.9.5artifacts contain both PRs' safeguards and register the requiredDisplayVersionandDisplayName.What the existing proof does not establish.
StartupRefusedcapture is a mounted-window render, not a real refusal by Windows. That path is covered by unit tests only.f0017f4devidence, not current-head captures. Those Store-side screenshots predate the Settings card redesign, the Inno consent rewrite, and the icon fix. The lifecycle they depict is re-proven at the current head above.Known and accepted.
MigrationRecordStorage.CreateProtectedDirectory, used by the consent store, does not yet carry PR 1's DACL scrubbing. Pre-existing, not a merge regression.Incident disclosure.
Rubber-duck review
The correctness hardening above came from a bounded independent adversarial review of the unpushed commits, which refuted five of six self-reported claims and found the four fail-open defects. Every fix it prompted carries a regression test, and the junction test was verified red-to-green against the actual pre-fix guard.
The scoped 11-file concise UI change received a bounded read-only, non-Codex rubber-duck review with no actionable regressions. Earlier foundation/shipping integration reviews remain historical evidence. The current UI review does not claim official release or gateway acceptance.
The three-file icon follow-up received direct source review against the existing
ChatWindowicon assignment pattern, with no actionable findings. No new second-model review is claimed for this one-line production change.The structured autoreview helper was unavailable: Claude/Pi executables were absent and Codex was excluded by request. No successful helper run is claimed.
Security Impact
Compatibility and Migration
DisplayVersionandDisplayNamecheck above is required.Migration.Build.propscarries build-onlyMigrationProductionEnabled=true, pinnedMigrationMinimumSourceVersion=2026.9.5.0, andMigrationStoreProductId=9NFPR3BGDRR5. Explicit Debug preview properties remain separate. No runtime or environment-variable production-enablement setting. CI gains one staging step, one artifact, and one contract-test step.SetupWindow, provision a replacement gateway, or bypass the pre-services startup guard.Review Conversations