Skip to content

[Feature]: Add worktree-isolated identity to winapp run #763

Description

Update: v1 shipped as a simpler staged-manifest transform without ownership records, locks, or journals. See the v1 scope revision comment; sections below about ownership metadata, sidecars, and locks no longer apply.

Is your feature request related to a problem? Please describe.

Multiple coding agents can build and run the same packaged .NET/WinUI app from different Git worktrees on one Windows machine. The worktrees already have separate build outputs and separate loose-layout AppX directories, but their manifests retain the same package identity. Windows therefore treats them as one package family: running one worktree can unregister or replace another worktree's development deployment, redirect AUMID activation, and share package-scoped state.

The current removal paths also include name-scoped operations that can remove more than the exact deployment selected by the caller. This is unsafe once several managed development deployments may coexist.

Describe the solution you'd like

Add explicit opt-in support:

winapp run <input> --unique-identity

The flag lets agents run the same packaged app concurrently from different worktrees. The caller chooses only whether to opt in; winapp resolves and manages the effective identity automatically.

Goals

  • Allow concurrent loose-layout development deployments of the same packaged .NET/WinUI app from different worktrees.
  • Preserve existing winapp run behavior unless --unique-identity is supplied.
  • Give the same resolved app in the same worktree a stable identity across reruns, regardless of whether it was invoked through ., a .csproj, or solution selection.
  • Keep each worktree's package registration, PFN/AUMIDs, and package-family-scoped app data isolated.
  • Modify only winapp's staged loose-layout copy. Never modify source files or the host build output.
  • Register and unregister only the exact development package owned by winapp.
  • Preserve localized packaged WinUI resources, including resources.pri.

Non-goals for v1

  • Automatic enablement based on detecting a Git worktree. Agents explicitly pass --unique-identity.
  • Unpackaged apps, which have no package identity to vary.
  • Sparse/external-location identity. Sparse identity requires matching <msix> identity embedded into the executable, rewriting the built EXE with mt.exe, invalidating signatures, and creating/signing another identity package.
  • MSIX/MSIXBundle installation, Store packages, package conversion, or production package rebranding.
  • Bundles, resource packages, optional packages, or multi-application package selection.
  • Rewriting public activation contracts such as protocols, file associations, or COM CLSIDs. (Execution aliases are the one exception; see below.)
  • Sharing or migrating app data between original and derived identities.
  • Using the derived identity as a public UI-automation selector.

UX and compatibility

  • winapp run without the flag preserves the manifest's original identity.
  • --unique-identity is valid only for packaged folder mode and packaged .NET/WinUI project mode.
  • It is rejected for unpackaged projects, sparse manifests, bundle inputs, and unsupported package shapes before registration.
  • Equivalent invocations that resolve to the same app use the same identity. The user does not choose an identity key.
  • The existing per-worktree loose-layout directory remains the staging directory: by default <resolved build output>\AppX, or the existing --output-appx-directory value.
  • Switching the same staging directory between normal and unique modes first unregisters the exact prior winapp-managed deployment, then rewrites the staged layout. Two registered identities must never reference the same mutable manifest directory.
  • Execution aliases under --unique-identity: each plain windows.appExecutionAlias on the single application is renamed in the staged layout only, and registration fails before any change if Windows already gives the renamed alias to a different package family. An authored alias foo.exe becomes foo.w<suffix>.exe, where <suffix> is the same 24-hex-character path-derived suffix used in the unique package name; winapp's generated default alias (winapp-<family>.exe) is regenerated from the unique package family instead. --with-alias launches through the renamed alias. (Scope expanded during review of Add worktree-isolated identity to winapp run #872.)
  • --clean removes data only for the exact effective development package.
  • --unregister-on-exit unregisters only the exact full package name returned by this run.

Deterministic identity algorithm

  1. Resolve the input using the existing run pipeline.
    • Project mode: use the canonical resolved .csproj path selected by project/solution resolution.
    • Folder mode: use the canonical resolved input folder.
  2. Canonicalize the path for Windows identity purposes: absolute path, normalized separators and trailing separator, case-insensitive normalization, and final filesystem target resolution where supported so junction, symlink, short-name, and equivalent path spellings converge.
  3. Read the original manifest identity. The stable family seed is the canonical app path, original Identity/@Name, and exact-case Identity/@Publisher, prefixed by an algorithm version.
  4. Hash the UTF-8 seed with SHA-256 and encode a fixed prefix using a package-name-safe lowercase alphabet.
  5. Set the effective name to <truncated-original>.w<hash>, preserving as much of the original name as possible while staying within the MSIX Identity Name limit of 3-50 characters.
  6. Validate the final package string against Windows rules: only ASCII letters, digits, . and -; no reserved device names or punycode-reserved forms; no trailing dot.
  7. Preserve Publisher, Version, ProcessorArchitecture, ResourceId, and every Application/@Id. Only Identity/@Name changes.
  8. Use Windows package identity APIs such as PackageFamilyNameFromId/PackageFullNameFromId rather than duplicating PFN/full-name algorithms. Each AUMID is <effective PFN>!<Application Id>.
  9. If the derived identity is already associated with a different canonical owner path, fail. Never add a random salt or silently adopt/remove the other deployment.

The algorithm and suffix length must be versioned and covered by golden tests. Once released, changing it is a migration because it changes package family and app-data location.

Transform pipeline

  1. Build/evaluate exactly as winapp run does today. Do not inject identity into source, project properties, or build output.
  2. Copy/synchronize the existing recipe-driven loose layout into the existing per-worktree AppX staging directory.
  3. Acquire a per-layout/per-effective-identity lock so concurrent invocations cannot race manifest transformation, registration, or sidecar updates.
  4. Read and preflight the staged manifest before changing it.
  5. Rewrite only staged Identity/@Name through AppxManifestDocument.
  6. If resources.pri exists, regenerate it under the effective package name while preserving all resource names, qualifiers, values, languages, and paths from the MSBuild-produced PRI. This step is mandatory and fail-closed.
  7. Validate the transformed manifest, effective package identity, PRI root map/languages, executable and assets before registration.
  8. Unregister only the exact prior package recorded for this layout when replacement is required.
  9. Register the staged manifest in DevelopmentMode and capture the resulting exact package full name and install location.
  10. Compute/select the AUMID and launch through the existing path.
  11. Atomically persist ownership metadata only after successful registration. On failure, retain enough prior metadata to clean up safely and report any orphaned exact full name.

Normal fully packaged WinUI/.NET loose-layout apps do not require an embedded <msix> fusion-manifest identity. Embedded identity is a sparse-package concern and is deferred.

PRI/resource fidelity requirement

Changing Identity/@Name changes the default authority for ms-resource and ms-appx resolution. Copying the original identity-bound resources.pri is unsafe, while the current fallback PRI generator indexes only a limited set of image resources and cannot reconstruct a general localized MSBuild/WinUI resource graph.

Before implementation, complete a blocking spike using makepri's PRI indexer:

  1. Build a packaged WinUI sample containing multiple .resw languages, manifest ms-resource: values, XAML resources, qualified images, and ms-appx references.
  2. Stage its MSBuild-produced layout and preserve the original PRI as a separate input.
  3. Generate a new PRI with an explicit /in <effective-package-name> and a config that indexes the existing PRI without auto-splitting resource packages.
  4. Compare detailed dumps and prove that the new root map uses the effective identity and that all languages, candidates, qualifiers, values, and paths survive.
  5. Register and launch two renamed worktree layouts concurrently and exercise localized UI/resource lookup.

If re-indexing the existing PRI cannot preserve full fidelity, the fallback design is an MSBuild-native temporary identity substitution before MakePri runs. The feature must not ship with best-effort PRI generation or a warning-only failure.

Supported and unsupported manifest surfaces

V1 supports package-scoped activation that becomes isolated through the new PFN/AUMID. It does not silently rewrite public contracts.

Preflight all application-level and package-level extensions. Conservatively fail --unique-identity when the manifest declares a surface whose concurrent behavior cannot be proven isolated, including:

  • uap5:AppExecutionAlias with anything beyond a plain ExecutionAlias (plain aliases are renamed; see above)
  • packaged COM servers/extensions and CLSIDs
  • protocols and file type associations
  • Windows services
  • startup tasks
  • shell/context-menu integrations
  • any other extension that registers a machine/user-global public name

App services, background tasks, notifications, and similar PFN/AUMID-scoped surfaces may be allowed only after their scoping is verified. Emit a clear warning that external clients hardcoding the original PFN/AUMID will not target the unique deployment.

Errors must name the unsupported manifest element/category and explain that winapp did not rewrite its public contract. V1 has no --allow-conflicts escape hatch.

Ownership, conflicts, reruns, and cleanup

Persist an atomic sidecar adjacent to the staging directory, not inside package content. Suggested fields:

  • schema/algorithm version
  • identity mode
  • canonical owner app path
  • staging layout path
  • original package identity values
  • effective package identity values
  • effective PFN, application ID/AUMID, and exact package full name
  • registered install location
  • transformed manifest hash
  • registration/update timestamps

Safety rules:

  • Treat the sidecar as a hint, not authority. Before removal verify the installed package's exact full name, IsDevelopmentMode, and install location matches the owned staging directory.
  • Use UnregisterByFullNameAsync/RemovePackageAsync(fullName, ...); never remove by name, prefix, family, or an enumeration wider than the selected exact package.
  • Never remove Store/signed/non-development packages.
  • A rerun with the same effective identity and unchanged manifest/layout may use the existing skip-registration optimization.
  • Payload-only updates synchronize the staged files without changing identity.
  • Manifest/identity changes perform exact unregister then register.
  • If another active winapp-managed deployment owns the original or effective identity from a different path, fail with its full name and owner/install path. For an original-identity collision, guide the caller to rerun with --unique-identity; never silently evict the other worktree.
  • If the same layout switches identity mode, remove its exact previously owned deployment before changing the staged manifest.
  • On hash collision, report both canonical paths and fail rather than changing the deterministic identity.

Explicit winapp unregister should consult matching ownership metadata and remove exact owned deployments only. Whether cleanup uses an added unregister --unique-identity flag or transparent sidecar discovery is an implementation UX decision, but name-scoped removal is not acceptable.

JSON additions

Keep the existing top-level AUMID, ProcessId, and Error fields. Add an optional additive Identity object for packaged runs:

{
  "AUMID": "Effective.Name_abcd1234!App",
  "ProcessId": 1234,
  "Identity": {
    "Mode": "Unique",
    "OriginalPackageName": "Original.Name",
    "EffectivePackageName": "Original.Name.w0123456789abcdef",
    "Publisher": "CN=Contoso",
    "PackageFamilyName": "Original.Name.w0123456789abcdef_abcd1234",
    "PackageFullName": "...",
    "ApplicationId": "App",
    "OwnerPath": "C:\\repo-worktree\\App.csproj",
    "LayoutPath": "C:\\repo-worktree\\bin\\Debug\\...\\AppX"
  }
}

Do not expose the internal hash/key as a UI-targeting contract. Human output should identify unique mode and the effective PFN/AUMID, with paths reserved for verbose output and conflict guidance.

Errors and exit behavior

Return nonzero, with equivalent structured --json errors, for:

  • unpackaged, sparse, bundle, multi-app, or otherwise unsupported input
  • unsupported public/global activation surface
  • invalid or over-length derived identity
  • canonicalization failure that prevents a stable key
  • managed identity owned by another path
  • PRI transformation or fidelity validation failure
  • registration state that cannot be proven safe to replace/remove
  • sidecar/installed-package disagreement

Errors should be actionable and include the relevant owner path, install location, exact package full name, manifest category/XPath, and safe remediation when available.

Fidelity and security caveats

  • Package-family-scoped WinRT app data is isolated automatically. Raw Win32 writes to shared %LOCALAPPDATA%, registry locations, files, ports, mutexes, named pipes, databases, and external services are not isolated by package identity.
  • Capability consent, notifications, protocol defaults, and other OS/user state do not migrate from the original identity.
  • Canonical path handling must resist alternate spellings, junctions, symlinks, 8.3 names, and case differences.
  • Sidecar tampering must never authorize broader removal; verify live package metadata before destructive operations.
  • Registration and sidecar writes require locking and atomic replacement for concurrent agents and crash recovery.
  • DevelopmentMode requires a supported local layout filesystem/location. Surface platform errors clearly.
  • Never log secrets from MSBuild properties or package configuration.

Implementation areas

  • RunCommand.cs / RunCommand.ProjectMode.cs: option, validation, threading, UX, JSON, and exact cleanup.
  • MsixService.Identity.cs / skip-registration path: staged transform, ownership-aware rerun, exact replacement.
  • AppxManifestDocument.cs: all-application identity/extension inspection and safe staged identity mutation.
  • PriService.cs / MrtAssetHelper.cs: identity-aware PRI re-indexing, dump validation, and fidelity checks.
  • PackageRegistrationService.cs / IPackageRegistrationService.cs: exact full-name lookup/removal and live ownership verification.
  • UnregisterCommand.cs: eliminate name-scoped removal and integrate managed sidecars.
  • Run result models/source-generated JSON context.
  • npm RunOptions, generated docs/schema, usage docs, plugin skills, and relevant samples.

Test matrix

Unit tests:

  • deterministic derivation and algorithm-version golden vectors
  • equivalent invocation forms resolving to one identity
  • different worktree paths producing different identities
  • case, separators, trailing separators, symlinks/junctions/short paths
  • package-name truncation and every Windows validation boundary
  • manifest rewrite changes only Identity/@Name
  • unsupported extension detection across manifest namespaces
  • sidecar atomicity, stale/corrupt state, lock contention, and crash recovery
  • exact full-name removal only; non-development and wrong-location packages are never removed
  • normal ↔ unique mode switching in the same layout
  • additive JSON success/error shapes

Integration tests on Windows:

  • two worktrees of one packaged .NET/WinUI app register and run concurrently
  • AUMIDs launch the correct physical layouts
  • package-family-scoped app data is distinct and stable across reruns
  • localized .resw, XAML, manifest strings, MRT-qualified assets, and ms-appx references retain fidelity
  • --clean and --unregister-on-exit affect only one worktree
  • default run detects another managed owner and recommends --unique-identity
  • DeveloperMode disabled and unsupported filesystem/location failures
  • x64, x86, and arm64 where CI permits
  • explicit --output-appx-directory
  • unsupported protocol/file-association/COM/service/startup manifests, and alias declarations beyond a plain ExecutionAlias, fail before registration

Sample/guide test:

  • clone/copy a packaged WinUI sample into two worktree-like paths, run both with --unique-identity --no-launch --json, assert different effective PFNs and exact registrations, then clean each independently.

Required pre-implementation spikes

  1. Prove lossless identity-aware PRI re-indexing for localized packaged WinUI as described above.
  2. Measure duplicate registration behavior for execution aliases, packaged COM CLSIDs, protocols/file associations, app services, services, and startup tasks; use results to refine the conservative support table.
  3. Validate canonical path resolution across junctions, symlinks, subst drives, 8.3 paths, and worktrees on common developer filesystems.
  4. Verify Windows package identity API use under the CLI's NativeAOT constraints.
  5. Verify exact rerun/removal behavior when a development package is active or files are in use.

Additional context

Current repository behavior already provides most of the needed seams:

  • Folder/project runs stage into separate per-worktree AppX directories, but register the unchanged package identity.
  • MSBuild-generated layouts are copied from .build.appxrecipe, preserving packaged files and existing localized PRI artifacts.
  • AppxManifestDocument already provides namespace-aware identity access.
  • PackageRegistrationService.UnregisterByFullNameAsync already exists, but several run/unregister paths still call broader name-scoped removal.
  • Current fallback PRI generation is intentionally insufficient for this feature because it does not preserve a general localized WinUI resource graph or set an explicit effective index name.
  • Normal packaged loose-layout WinUI apps do not need embedded <msix> identity; sparse apps do and are deferred.

Relevant Windows rules:

  • MSIX Identity/@Name is 3-50 characters and forms the PFN with the publisher ID.
  • AUMID is <package family name>!<Application Id>.
  • DevelopmentMode loose-layout registration does not require package signing, but requires a supported local development layout and cannot be used with bundles.
  • resources.pri top-level resource-map authority typically corresponds to package identity and must be regenerated when the identity name changes.
  • Package removal APIs operate on exact package full names; app-data preservation is valid only for DevelopmentMode packages.

Open implementation decisions that should be resolved by the spikes rather than guessed:

  • exact safe hash encoding/length and canonical filesystem API
  • PRI re-index configuration versus temporary MSBuild identity injection fallback
  • explicit versus sidecar-discovered winapp unregister UX
  • which currently conservative manifest surfaces can be safely supported after measured conflict behavior

Activity

  1. azchohfi commented on Sep 18, 2026

    @azchohfi
    Collaborator

    Data points from a consumer that hit this exact problem and ended up solving it locally, in case they are useful for the two blocking spikes. We are a packaged WinUI test host in microsoft/microsoft-ui-reactor; the change is microsoft/microsoft-ui-reactor#1260.

    1. The uap5:AppExecutionAlias rule excludes us, and alias support may not be separable from the goal.

    We are exactly the target audience (two git worktrees, one packaged WinUI app, agents running them concurrently), but our manifest declares an execution alias, so v1 would conservatively fail us per the unsupported-surface table.

    For us the alias is not cosmetic. Launching the alias stub keeps package identity and inherits stdout/stderr, argv and exit code; AUMID activation is brokered, so stdout cannot be redirected at all. Our whole packaged test tier depends on capturing TAP output from a process that has identity, so "register with --unique-identity, launch by AUMID" is not a workaround for this shape of app.

    More relevant to the design: deriving only Identity/@Name does not actually isolate an app that declares an alias. The alias is a single filename in a flat per-user directory (%LOCALAPPDATA%\Microsoft\WindowsApps, 66 flat entries on this machine alongside 27 PFN-scoped subdirectories). Two worktrees would get distinct PFNs and app data, then still contend for one stub, so whichever registered last owns the name. We had to derive the alias as well as the name to get the isolation the flag is for. That is a concrete case for spike 2: for alias-declaring apps, refusing the flag and rewriting the alias may be the only two coherent options, and refusing leaves the stated goal unmet.

    2. PRI fidelity: a narrow counter-data-point, which does not clear spike 1.

    We rename staged Identity/@Name and do not regenerate resources.pri (1.36 MB, merged framework PRIs including Microsoft.UI.Xaml.Controls.pri). 1584 packaged fixtures pass under the renamed identity.

    The load-bearing one is a fixture that resolves ms-appx:///Assets/...ico through MRT under package identity and compares the resulting icon pixels against the same file addressed by filesystem path, with a zero control proving the probe can still observe "no icon". It is not a liveness check, and it passed renamed.

    Bounding that honestly, because it is the part that matters: we have zero .resw, no ms-resource: resolution under package identity, and no multi-language candidates. So this is evidence only for the unlocalized case where the PRI is framework content plus assets. It says nothing about the localized MSBuild/WinUI resource graph your spike is actually about, and it is not a reason to relax the fail-closed requirement.

    What it might be worth: the unlocalized case appears to survive an identity rename with the original PRI intact. If that generalizes, "no .resw and no ms-resource:" could be a cheap preflight that skips PRI re-indexing entirely rather than blocking the feature on the general solution.

    3. Cheap corroboration on name-scoped removal.

    Independently of this issue we had the bug described in the cleanup section: removal scoped to name plus publisher, which evicted another worktree's live registration. Agreeing from experience that name-scoped removal is not acceptable. What we landed on was a single publisher-filtered sweep with three rules (this layout's derived name; any package installed from this exact directory; derived-shaped packages whose install directory no longer exists), none of which can reach a live concurrent checkout.

    The second rule turned out to be necessary rather than nice-to-have: without it, the first run after adopting derived identities finds a stale base-name registration owning the same layout directory, and registering over it fails in a way that killed our host mid-run. Worth considering for the mode-switch path, which has the same shape.

    The third matters more once identities are per-path: one shared registration becomes one per worktree, so deleting a worktree leaks a registration unless something reclaims it.

  2. nmetulev commented on Oct 2, 2026

    @nmetulev
    MemberAuthor

    Confirming the expanded v1 scope from review of #872: execution aliases are in scope for --unique-identity.

    • Supported: a plain windows.appExecutionAlias extension (one AppExecutionAlias with plain ExecutionAlias elements) on the manifest's single application. Anything richer is still rejected before registration.
    • Renamed in staging only: the source manifest is unchanged; the staged appxmanifest.xml gets the derived alias.
    • Naming rule: an authored alias foo.exe becomes foo.w<suffix>.exe, where <suffix> is the same 24-hex-character suffix used for the unique package name (first 12 bytes of SHA-256 over the canonical checkout path, original Identity/@Name, and publisher). The stem is truncated if needed to stay a valid filename. If the authored alias is winapp's own generated default (winapp-<family>.exe), it is regenerated from the unique package family instead. The name is stable for a given checkout path, and run prints each original -> renamed mapping.
    • Ownership check: before any package change, winapp checks each renamed alias in %LOCALAPPDATA%\Microsoft\WindowsApps. If it exists and its owner is not the expected unique package family (or the owner can't be verified), the run fails and nothing is changed.
    • --with-alias works under --unique-identity and launches through the renamed alias.

    I've updated the non-goals, the --with-alias note, and the support table in the issue body to match.

  3. nmetulev commented on Oct 8, 2026

    @nmetulev
    MemberAuthor

    v1 scope revision (#872). The implementation was simplified after review, and this supersedes my earlier comment on aliases and naming.

    What v1 does:

    • winapp run --unique-identity derives <first 24 chars of Identity/@Name>.w<24 hex> from the canonical checkout path (the .csproj/.vcxproj/.cs file, or the input folder) and the original name. The publisher is not part of the hash, because Windows already keeps packages from different publishers apart by package family.
    • Only the staged layout changes: Identity/@Name, authored execution aliases (tool.exe -> tool.w<hash>.exe), and resources.pri, which is re-indexed with one makepri call. Then the normal register flow runs under the derived name.
    • The generated default alias comes from the new package family. Alias availability is checked before launching through an alias, the same as in a plain run.
    • unregister with the same project, .cs file, or folder also looks for the derived name. No extra flag is needed.
    • Sandbox: the host stages the renamed layout and the guest registers it unchanged. No guest-agent changes.

    Dropped from the original spec: ownership metadata/sidecars, locks, atomic journals and crash recovery, revision fencing, and guest capability negotiation. The derived name already encodes the checkout path, so Windows' registration state is enough, and plain winapp run stays exactly as before. Spec sections on those mechanisms (ownership, sidecar atomicity, lock contention, ownership-aware cleanup) no longer apply to v1.

  4. added a commit that references this issue on Oct 9, 2026
    11eecfb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions