@rightkit/release 0.2.151 → 0.2.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE-APACHE +201 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +117 -8
  4. package/build-release.mjs +7 -2
  5. package/cli/right-release.mjs +18 -5
  6. package/committed-source.mjs +19 -1
  7. package/crate-graph.mjs +121 -0
  8. package/deps.mjs +0 -0
  9. package/development-signing.mjs +8 -0
  10. package/development.mjs +46 -13
  11. package/devtools-policy.json +5 -0
  12. package/devtools-policy.mjs +119 -0
  13. package/direct-bootstrap.mjs +9 -3
  14. package/dmg-build.mjs +19 -5
  15. package/dmg-layout.mjs +2 -2
  16. package/dmg-spec.mjs +97 -0
  17. package/execution-policy.mjs +17 -1
  18. package/github-release.mjs +25 -7
  19. package/github-upload-gate.mjs +84 -0
  20. package/hardeningscan.mjs +3 -0
  21. package/inventory-check.mjs +4 -4
  22. package/lsclean.sh +0 -0
  23. package/mac-bundle-signing.mjs +1 -1
  24. package/mac-sign-core.mjs +97 -0
  25. package/mac-signing-matrix.mjs +186 -0
  26. package/msix-appinstaller.mjs +4 -1
  27. package/msix-build.mjs +3 -6
  28. package/msix-config.mjs +5 -1
  29. package/msix-repair-guard.mjs +24 -0
  30. package/msix-repair-guard.ps1 +356 -0
  31. package/msix-signer.mjs +211 -0
  32. package/ownership-catalog-config.json +6 -7
  33. package/ownership-command.mjs +69 -8
  34. package/ownership-evidence.mjs +1 -1
  35. package/ownership-key-authority.mjs +1 -1
  36. package/ownership-key-command.mjs +15 -3
  37. package/ownership-key-elevation.mjs +11 -2
  38. package/ownership-mandatory-set.json +420 -0
  39. package/ownership-mandatory-set.mjs +273 -0
  40. package/ownership.mjs +2 -2
  41. package/package.json +25 -11
  42. package/packaged-app-identity.mjs +88 -0
  43. package/preflight.mjs +25 -3
  44. package/prune-r2.mjs +28 -3
  45. package/publication-gate.mjs +49 -10
  46. package/publish-cargo.mjs +21 -1
  47. package/publish-swift.mjs +78 -141
  48. package/publish-update.mjs +24 -2
  49. package/qa-build-features.mjs +216 -0
  50. package/qa-control-scan.mjs +435 -0
  51. package/registry-parity.mjs +18 -1
  52. package/release-invocation.mjs +2 -1
  53. package/release-state.mjs +9 -0
  54. package/release-switch-registry.json +33 -0
  55. package/release-switch-scan.mjs +452 -0
  56. package/release.mjs +320 -26
  57. package/renamed-away.mjs +29 -0
  58. package/rightkit-ownership.json +682 -709
  59. package/rightkit-sync.mjs +8 -4
  60. package/rightkit-version-policy.mjs +18 -0
  61. package/rightkit-versions.json +211 -127
  62. package/runtime-budget-classes.json +4 -0
  63. package/runtime-budgets.mjs +870 -0
  64. package/runtime-bundle-scan.mjs +61 -0
  65. package/sign-macos.mjs +28 -30
  66. package/sign-updater.mjs +5 -2
  67. package/swift-mac-dmg.mjs +7 -2
  68. package/swift-package-command.mjs +1 -1
  69. package/upload-large.mjs +0 -0
  70. package/upload-release.mjs +10 -37
@@ -0,0 +1,356 @@
1
+ # RightKit MSIX repair guard (canonical source: rightkit/packages/release/msix-repair-guard.ps1). ASCII only; Windows PowerShell 5.1 compatible.
2
+ #
3
+ # Why this exists: cancelling an MSIX update while Windows registers it can leave the OLD package
4
+ # "Modified, NeedsRemediation" (event 751: "Windows failed to undo changes ... The package needs to be reinstalled").
5
+ # A package in that state cannot be launched, so nothing inside the package can repair it. This script lives OUTSIDE the
6
+ # package (the app writes it to %LOCALAPPDATA%\<AppName>\repair\ on every normal launch) and is run by a per-user scheduled
7
+ # task `<PFN>-Repair` (no admin) on event 751 in Microsoft-Windows-AppXDeploymentServer/Operational, at logon, and at a
8
+ # one-shot retry time after a failed repair.
9
+ #
10
+ # msix-repair-guard.ps1 -Install -Pfn <family> [-Feed <.appinstaller uri>] register/refresh the task and write guard.json
11
+ # msix-repair-guard.ps1 (task action) repair if needed, clean up if uninstalled
12
+ #
13
+ # Repair order: `Add-AppxPackage -AppInstallerFile <feed>` (known-good), then `Add-AppxPackage -Path <msix>` (the msix is downloaded
14
+ # first and its manifest identity checked). Every decision is logged to repair.log next to this script; every run also writes a
15
+ # machine-readable last-result.json (status, code, time, detail), because the task runs under `conhost --headless` and hides exit codes.
16
+ #
17
+ # Exit codes (also in last-result.json): 0 healthy / repaired / cleaned up / another run active, 1 repair failed (a retry is scheduled),
18
+ # 2 guard misconfigured or refused (guard.json or an install input failed validation), 3 no install source known, 4 unhandled error
19
+ # (logged; a retry is scheduled).
20
+ #
21
+ # Retry: after a failed repair (exit 1 or 4) the script gives the task a one-shot time trigger at +2, +5, +15, +60 minutes and then
22
+ # hourly, for at most 24 hours from the first failure; the trigger is removed on success, when the package is healthy, or when the
23
+ # app is uninstalled. State: retry.json. A later logon or event 751 run still starts a repair at any time.
24
+ #
25
+ # Uninstall cleanup: a run that finds the package absent (twice, 10 s apart) removes this folder and the task. It runs at the next
26
+ # logon, the next event 751, or the next retry tick; Windows has no "package removed" trigger for a per-user task.
27
+ #
28
+ # Hardening: feed and msix URIs must be https, or http to a loopback host (a local test feed). A feed or msix whose identity (Name,
29
+ # Publisher) differs from the installed package is refused and never installed. guard.json must name this folder's task.
30
+ #
31
+ # Requirement: the app's data directory must be physical (MSIX fileSystemWrite virtualization disabled), otherwise the packaged app
32
+ # writes this folder into its private LocalCache and the task cannot see it.
33
+ [CmdletBinding()]
34
+ param([switch]$Install, [string]$Pfn = '', [string]$Feed = '')
35
+ $ErrorActionPreference = 'Stop'
36
+ $ProgressPreference = 'SilentlyContinue' # Windows PowerShell 5.1 renders a progress bar per downloaded chunk: a 30 MB Invoke-WebRequest -OutFile then takes minutes
37
+ $GuardVersion = 2
38
+ $Dir = Split-Path -Parent $PSCommandPath
39
+ $LogPath = Join-Path $Dir 'repair.log'
40
+ $ConfigPath = Join-Path $Dir 'guard.json'
41
+ $RetryPath = Join-Path $Dir 'retry.json'
42
+ $AttemptPath = Join-Path $Dir 'install-attempt.json'
43
+ $ResultPath = Join-Path $Dir $(if ($Install) { 'install-result.json' } else { 'last-result.json' })
44
+ $Mode = $(if ($Install) { 'install' } else { 'repair' })
45
+ $RetryDelaysMinutes = @(2, 5, 15, 60) # then hourly
46
+ $RetryWindowHours = 24
47
+ $PfnPattern = '^[A-Za-z0-9][A-Za-z0-9.\-]*_[a-z0-9]{13}$'
48
+ $script:Ctx = ''
49
+
50
+ function Log([string]$m) {
51
+ try {
52
+ if ((Test-Path -LiteralPath $LogPath) -and ((Get-Item -LiteralPath $LogPath).Length -gt 262144)) { Move-Item -LiteralPath $LogPath -Destination ($LogPath + '.old') -Force }
53
+ Add-Content -LiteralPath $LogPath -Value ((Get-Date).ToString('yyyy-MM-ddTHH:mm:ss.fffzzz') + ' [' + $Mode + '] ' + $m)
54
+ } catch { }
55
+ }
56
+ function Write-Json([string]$path, $obj) {
57
+ try {
58
+ $tmp = $path + '.tmp'
59
+ [IO.File]::WriteAllText($tmp, ($obj | ConvertTo-Json), (New-Object Text.UTF8Encoding($false)))
60
+ Move-Item -LiteralPath $tmp -Destination $path -Force
61
+ } catch { Log ('could not write ' + $path + ': ' + $_.Exception.Message) }
62
+ }
63
+ function Write-Result([string]$status, [int]$code, [string]$detail) {
64
+ $r = [ordered]@{ status = $status; code = $code; mode = $Mode; timeUtc = (Get-Date).ToUniversalTime().ToString('o'); guardVersion = $GuardVersion; detail = $detail }
65
+ $st = Read-Json $RetryPath
66
+ if ($st -and $st.nextEpoch) { $r.nextRetryUtc = ([datetime]'1970-01-01T00:00:00Z').ToUniversalTime().AddSeconds([double]$st.nextEpoch).ToString('o'); $r.retryAttempt = [int]$st.attempt }
67
+ Write-Json $ResultPath $r
68
+ }
69
+ function Finish([string]$status, [int]$code, [string]$detail) {
70
+ Log ($status.ToUpper() + ' (code ' + $code + '): ' + $detail)
71
+ Write-Result $status $code $detail
72
+ exit $code
73
+ }
74
+ function Read-Json([string]$path) { try { return (Get-Content -LiteralPath $path -Raw -ErrorAction Stop | ConvertFrom-Json) } catch { return $null } }
75
+ function Read-Config { return (Read-Json $ConfigPath) }
76
+
77
+ # ------------------------------------------------------------------ scheduled task (shared by install and the retry logic)
78
+ function Get-TaskXml([string]$family, $retryLocal) {
79
+ $id = [Security.Principal.WindowsIdentity]::GetCurrent()
80
+ $esc = { param($s) [Security.SecurityElement]::Escape([string]$s) }
81
+ $sub = '<QueryList><Query Id="0" Path="Microsoft-Windows-AppXDeploymentServer/Operational"><Select Path="Microsoft-Windows-AppXDeploymentServer/Operational">*[System[(EventID=751)]]</Select></Query></QueryList>'
82
+ $taskArgs = '--headless powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $PSCommandPath + '"'
83
+ $retryXml = ''
84
+ if ($retryLocal) { $retryXml = '<TimeTrigger><StartBoundary>' + ([datetime]$retryLocal).ToString('yyyy-MM-ddTHH:mm:ss') + '</StartBoundary><Enabled>true</Enabled></TimeTrigger>' }
85
+ return @"
86
+ <?xml version="1.0" encoding="UTF-16"?>
87
+ <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
88
+ <RegistrationInfo><Description>Repair guard for $(& $esc $family): re-adds the app from its App Installer feed when an interrupted update leaves it NeedsRemediation; retries after a failure. Removes itself when the app is uninstalled.</Description></RegistrationInfo>
89
+ <Triggers>
90
+ <EventTrigger><Enabled>true</Enabled><Subscription>$(& $esc $sub)</Subscription></EventTrigger>
91
+ <LogonTrigger><Enabled>true</Enabled><UserId>$(& $esc $id.Name)</UserId></LogonTrigger>
92
+ $retryXml
93
+ </Triggers>
94
+ <Principals><Principal id="Author"><UserId>$(& $esc $id.User.Value)</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
95
+ <Settings>
96
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
97
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
98
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
99
+ <AllowHardTerminate>true</AllowHardTerminate>
100
+ <StartWhenAvailable>true</StartWhenAvailable>
101
+ <AllowStartOnDemand>true</AllowStartOnDemand>
102
+ <Enabled>true</Enabled>
103
+ <Hidden>true</Hidden>
104
+ <ExecutionTimeLimit>PT30M</ExecutionTimeLimit>
105
+ </Settings>
106
+ <Actions Context="Author"><Exec><Command>%SystemRoot%\System32\conhost.exe</Command><Arguments>$(& $esc $taskArgs)</Arguments></Exec></Actions>
107
+ </Task>
108
+ "@
109
+ }
110
+ function Register-GuardTask([string]$family, $retryLocal) {
111
+ [void](Register-ScheduledTask -TaskName ($family + '-Repair') -Xml (Get-TaskXml $family $retryLocal) -Force)
112
+ }
113
+ function Get-RetryLocal {
114
+ $st = Read-Json $RetryPath
115
+ if ($st -and $st.nextEpoch) { return ([datetime]'1970-01-01T00:00:00Z').ToUniversalTime().AddSeconds([double]$st.nextEpoch).ToLocalTime() }
116
+ return $null
117
+ }
118
+
119
+ # ------------------------------------------------------------------ retry state (repair mode)
120
+ function Clear-Retry([string]$family) {
121
+ if (-not (Test-Path -LiteralPath $RetryPath)) { return }
122
+ try { Remove-Item -LiteralPath $RetryPath -Force } catch { }
123
+ try { Register-GuardTask $family $null; Log 'retry cleared (task has no one-shot trigger)' } catch { Log ('could not clear the retry trigger: ' + $_.Exception.Message) }
124
+ }
125
+ function Set-Retry([string]$family, [string]$why) {
126
+ $epoch0 = [datetime]'1970-01-01T00:00:00Z'
127
+ $now = [datetime]::UtcNow
128
+ $nowEpoch = [math]::Floor(($now - $epoch0.ToUniversalTime()).TotalSeconds)
129
+ $st = Read-Json $RetryPath
130
+ $attempt = 1; $first = $nowEpoch
131
+ if ($st -and $st.firstFailureEpoch -and (($nowEpoch - [double]$st.firstFailureEpoch) -lt ($RetryWindowHours * 3600))) { $attempt = [int]$st.attempt + 1; $first = [double]$st.firstFailureEpoch }
132
+ $delay = $(if ($attempt -le $RetryDelaysMinutes.Count) { $RetryDelaysMinutes[$attempt - 1] } else { 60 })
133
+ $next = $nowEpoch + ($delay * 60)
134
+ if (($next - $first) -gt ($RetryWindowHours * 3600)) {
135
+ Write-Json $RetryPath ([ordered]@{ attempt = $attempt; firstFailureEpoch = $first; gaveUp = $true; reason = $why })
136
+ try { Register-GuardTask $family $null } catch { }
137
+ Log ('retry window of ' + $RetryWindowHours + ' h used up after ' + ($attempt - 1) + ' retries; no further automatic retry (next logon / event 751 starts again)')
138
+ return
139
+ }
140
+ Write-Json $RetryPath ([ordered]@{ attempt = $attempt; firstFailureEpoch = $first; nextEpoch = $next; delayMinutes = $delay; reason = $why })
141
+ $local = $epoch0.ToUniversalTime().AddSeconds($next).ToLocalTime()
142
+ try { Register-GuardTask $family $local; Log ('retry ' + $attempt + ' scheduled in ' + $delay + ' min at ' + $local.ToString('s')) } catch { Log ('could not schedule the retry: ' + $_.Exception.Message) }
143
+ }
144
+
145
+ # ------------------------------------------------------------------ package state
146
+ function Get-Family([string]$family) {
147
+ $name = $family.Substring(0, $family.LastIndexOf('_'))
148
+ return @(Get-AppxPackage -Name $name -PackageTypeFilter Main -ErrorAction SilentlyContinue | Where-Object { [string]$_.PackageFamilyName -eq $family })
149
+ }
150
+ function Get-StateText([string]$family) {
151
+ $p = @(Get-Family $family)
152
+ if ($p.Count -eq 0) { return 'absent' }
153
+ return (($p | ForEach-Object { [string]$_.Version + ':' + [string]$_.Status }) -join ',')
154
+ }
155
+ function Test-Healthy([string]$family) {
156
+ $p = @(Get-Family $family)
157
+ return ($p.Count -gt 0 -and @($p | Where-Object { [string]$_.Status -ne 'Ok' }).Count -eq 0)
158
+ }
159
+ function Test-AllowedUri([string]$uri) {
160
+ try {
161
+ $u = [Uri]$uri
162
+ if (-not $u.IsAbsoluteUri -or $u.UserInfo) { return $false }
163
+ if ($u.Scheme -eq 'https') { return $true }
164
+ if ($u.Scheme -eq 'http' -and $u.IsLoopback) { return $true }
165
+ } catch { }
166
+ return $false
167
+ }
168
+ function Same-Text([string]$a, [string]$b) { return ([string]::Equals($a.Trim(), $b.Trim(), [StringComparison]::OrdinalIgnoreCase)) }
169
+ function Test-Identity($expected, [string]$name, [string]$publisher) {
170
+ if (-not $expected.name -or -not $expected.publisher) { return 'no expected identity recorded' }
171
+ if (-not (Same-Text $expected.name $name)) { return ('Name ' + $name + ' differs from ' + $expected.name) }
172
+ if (-not (Same-Text $expected.publisher $publisher)) { return ('Publisher ' + $publisher + ' differs from ' + $expected.publisher) }
173
+ return ''
174
+ }
175
+ function Get-FeedInfo([string]$feedUri) {
176
+ $r = Invoke-WebRequest -Uri $feedUri -UseBasicParsing -TimeoutSec 20 -MaximumRedirection 3
177
+ if (-not (Test-AllowedUri ([string]$r.BaseResponse.ResponseUri))) { throw ('feed redirected to a disallowed uri ' + $r.BaseResponse.ResponseUri) }
178
+ $text = $r.Content
179
+ if ($text -is [byte[]]) { $text = [Text.Encoding]::UTF8.GetString($text) } # a non-text content type returns bytes
180
+ $x = [xml]($text.TrimStart([char]0xFEFF))
181
+ $n = $x.GetElementsByTagName('MainPackage') | Select-Object -First 1
182
+ if (-not $n) { $n = $x.GetElementsByTagName('MainBundle') | Select-Object -First 1 }
183
+ if (-not $n) { throw 'feed has no MainPackage or MainBundle' }
184
+ return [pscustomobject]@{ uri = [string]$n.Uri; name = [string]$n.Name; publisher = [string]$n.Publisher; version = [string]$n.Version }
185
+ }
186
+ function Save-Msix([string]$uri, [string]$dest) {
187
+ $r = Invoke-WebRequest -Uri $uri -UseBasicParsing -TimeoutSec 900 -MaximumRedirection 3 -OutFile $dest -PassThru
188
+ if (-not (Test-AllowedUri ([string]$r.BaseResponse.ResponseUri))) { throw ('msix redirected to a disallowed uri ' + $r.BaseResponse.ResponseUri) }
189
+ }
190
+ function Get-MsixIdentity([string]$file) {
191
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
192
+ $zip = [IO.Compression.ZipFile]::OpenRead($file)
193
+ try {
194
+ $entry = $zip.GetEntry('AppxManifest.xml')
195
+ $kind = 'Package'
196
+ if (-not $entry) { $entry = $zip.GetEntry('AppxMetadata/AppxBundleManifest.xml'); $kind = 'Bundle' }
197
+ if (-not $entry) { throw 'no AppxManifest.xml or AppxBundleManifest.xml in the archive' }
198
+ $sr = New-Object IO.StreamReader($entry.Open())
199
+ try { $x = [xml]($sr.ReadToEnd().TrimStart([char]0xFEFF)) } finally { $sr.Dispose() }
200
+ $idn = $x.GetElementsByTagName('Identity') | Select-Object -First 1
201
+ if (-not $idn) { throw 'manifest has no Identity' }
202
+ return [pscustomobject]@{ name = [string]$idn.Name; publisher = [string]$idn.Publisher; version = [string]$idn.Version; kind = $kind }
203
+ } finally { $zip.Dispose() }
204
+ }
205
+
206
+ # ================================================================== install: register / refresh the task
207
+ if ($Install) {
208
+ trap { Log ('UNHANDLED ERROR: ' + $_.Exception.Message + ' at line ' + $_.InvocationInfo.ScriptLineNumber); Write-Result 'error' 4 $_.Exception.Message; exit 4 }
209
+ if (-not $Pfn) { Finish 'misconfigured' 2 'install refused: no -Pfn' }
210
+ if ($Pfn -notmatch $PfnPattern) { Finish 'refused' 2 ('install refused: -Pfn is not a package family name: ' + $Pfn) }
211
+ $prev = Read-Config
212
+ $feedUri = $Feed
213
+ try {
214
+ [void][Windows.Management.Deployment.PackageManager, Windows.Management.Deployment, ContentType = WindowsRuntime]
215
+ $pm = New-Object Windows.Management.Deployment.PackageManager
216
+ foreach ($x in @($pm.FindPackagesForUser('', $Pfn))) {
217
+ $ai = $x.GetAppInstallerInfo()
218
+ if ($ai -and $ai.Uri) { $feedUri = [string]$ai.Uri; break }
219
+ }
220
+ } catch { Log ('App Installer info unreadable: ' + $_.Exception.Message) }
221
+ if (-not $feedUri -and $prev -and $prev.feed) { $feedUri = [string]$prev.feed; Log 'package has no App Installer association; keeping the recorded feed' }
222
+ $msix = $(if ($prev -and $prev.msix) { [string]$prev.msix } else { '' })
223
+ if ($feedUri -and -not (Test-AllowedUri $feedUri)) { Finish 'refused' 2 ('install refused: feed uri is not https (or http to loopback): ' + $feedUri) }
224
+ if ($feedUri) {
225
+ try {
226
+ $fi = Get-FeedInfo $feedUri
227
+ if ($fi.uri) { if (Test-AllowedUri $fi.uri) { $msix = $fi.uri } else { Log ('feed msix uri not recorded (not https): ' + $fi.uri) } }
228
+ } catch { Log ('feed not readable at install time (' + $feedUri + '): ' + $_.Exception.Message) }
229
+ } else { Log 'no App Installer feed known: repair can only use a recorded msix uri' }
230
+ if ($msix -and -not (Test-AllowedUri $msix)) { Finish 'refused' 2 ('install refused: msix uri is not https (or http to loopback): ' + $msix) }
231
+
232
+ # expected identity: what is installed now (this run is started by the packaged app), else what was recorded
233
+ $name = $(if ($prev -and $prev.name) { [string]$prev.name } else { '' })
234
+ $publisher = $(if ($prev -and $prev.publisher) { [string]$prev.publisher } else { '' })
235
+ $pk = @(Get-Family $Pfn) | Select-Object -First 1
236
+ if ($pk) { $name = [string]$pk.Name; $publisher = [string]$pk.Publisher }
237
+ if (-not $name -or -not $publisher) { Log 'WARNING: package identity (Name, Publisher) unknown; the repair will refuse every source until a launch of the installed app records it' }
238
+
239
+ $taskName = $Pfn + '-Repair'
240
+ try { Register-GuardTask $Pfn (Get-RetryLocal) } catch { Finish 'misconfigured' 2 ('task registration failed: ' + $_.Exception.Message) }
241
+ $cfg = [ordered]@{ guardVersion = $GuardVersion; pfn = $Pfn; name = $name; publisher = $publisher; feed = $feedUri; msix = $msix; task = $taskName; dir = $Dir; installedUtc = (Get-Date).ToUniversalTime().ToString('o') }
242
+ [IO.File]::WriteAllText($ConfigPath, ($cfg | ConvertTo-Json), (New-Object Text.UTF8Encoding($false)))
243
+ try { Remove-Item -LiteralPath $AttemptPath -Force -ErrorAction SilentlyContinue } catch { }
244
+ Log ('task ' + $taskName + ' registered; identity=' + $name + ' / ' + $publisher + '; feed=' + $(if ($feedUri) { $feedUri } else { '(none)' }) + ' msix=' + $(if ($msix) { $msix } else { '(none)' }))
245
+ Write-Result 'installed' 0 ('task ' + $taskName + ' registered')
246
+ exit 0
247
+ }
248
+
249
+ # ================================================================== repair (task action)
250
+ trap {
251
+ Log ('UNHANDLED ERROR: ' + $_.Exception.Message + ' at line ' + $_.InvocationInfo.ScriptLineNumber)
252
+ Write-Result 'error' 4 $_.Exception.Message
253
+ if ($script:Ctx) { try { Set-Retry $script:Ctx 'unhandled error' } catch { } }
254
+ exit 4
255
+ }
256
+ $cfg = Read-Config
257
+ if (-not $cfg -or -not $cfg.pfn) { Finish 'misconfigured' 2 'guard.json missing or unreadable; nothing to do' }
258
+ $family = [string]$cfg.pfn
259
+ $feedUri = [string]$cfg.feed
260
+ $msix = [string]$cfg.msix
261
+ if ($family -notmatch $PfnPattern) { Finish 'refused' 2 ('guard.json pfn is not a package family name: ' + $family) }
262
+ if ([string]$cfg.task -ne ($family + '-Repair')) { Finish 'refused' 2 ('guard.json task ' + [string]$cfg.task + ' is not ' + $family + '-Repair') }
263
+ if ((Split-Path -Leaf $Dir) -ne 'repair') { Finish 'refused' 2 ('script folder is not named repair: ' + $Dir) }
264
+ if ($feedUri -and -not (Test-AllowedUri $feedUri)) { Finish 'refused' 2 ('guard.json feed uri is not https (or http to loopback): ' + $feedUri) }
265
+ if ($msix -and -not (Test-AllowedUri $msix)) { Finish 'refused' 2 ('guard.json msix uri is not https (or http to loopback): ' + $msix) }
266
+ $script:Ctx = $family
267
+ $mutex = [Threading.Mutex]::new($false, ('Local\' + $family + '-Repair'))
268
+ if (-not $mutex.WaitOne(0)) { Log 'another repair run is active; exiting (last-result.json left to that run)'; exit 0 }
269
+ $tmpMsix = ''
270
+ try {
271
+ $state = Get-StateText $family
272
+ if ($state -eq 'absent') {
273
+ Start-Sleep -Seconds 10 # an update or reinstall can briefly show no package; require absence twice
274
+ $state = Get-StateText $family
275
+ }
276
+ if ($state -eq 'absent') {
277
+ # uninstalled: remove the repair folder (never its parent: that holds user data), then the task itself (last: it ends this run)
278
+ $note = Join-Path $env:TEMP ($family + '-repair-cleanup.log')
279
+ try { Add-Content -LiteralPath $note -Value ((Get-Date).ToString('o') + ' package ' + $family + ' absent; removing task ' + $cfg.task + ' and ' + $Dir) } catch { }
280
+ try { Remove-Item -LiteralPath $Dir -Recurse -Force -ErrorAction Stop } catch { try { Add-Content -LiteralPath $note -Value ('folder removal failed: ' + $_.Exception.Message) } catch { } }
281
+ try { Unregister-ScheduledTask -TaskName ([string]$cfg.task) -Confirm:$false -ErrorAction Stop; Add-Content -LiteralPath $note -Value 'task unregistered' } catch { try { Add-Content -LiteralPath $note -Value ('task removal failed: ' + $_.Exception.Message) } catch { } }
282
+ exit 0
283
+ }
284
+ if (Test-Healthy $family) { Clear-Retry $family; Finish 'healthy' 0 ('package healthy: ' + $state) }
285
+ Log ('package NOT healthy: ' + $state + '; letting the deployment service settle')
286
+ Start-Sleep -Seconds 8
287
+ if (Test-Healthy $family) { Clear-Retry $family; Finish 'healthy' 0 ('package recovered by itself: ' + (Get-StateText $family)) }
288
+
289
+ if (-not $feedUri -and -not $msix) { Finish 'no-source' 3 'FAILED: no App Installer feed and no msix uri recorded; cannot repair. Reinstall from the original source or use Settings > Apps > Advanced options > Repair.' }
290
+ # expected identity: recorded at install; fall back to the (broken) installed registration, which still reports Name and Publisher
291
+ $expect = [pscustomobject]@{ name = [string]$cfg.name; publisher = [string]$cfg.publisher }
292
+ if (-not $expect.name -or -not $expect.publisher) {
293
+ $pk = @(Get-Family $family) | Select-Object -First 1
294
+ if ($pk) { $expect.name = [string]$pk.Name; $expect.publisher = [string]$pk.Publisher }
295
+ }
296
+ if (-not $expect.name -or -not $expect.publisher) {
297
+ Log 'FAILED: expected package identity unknown; refusing to install anything'
298
+ Set-Retry $family 'identity unknown'
299
+ Write-Result 'failed' 1 'expected package identity unknown'
300
+ exit 1
301
+ }
302
+ $why = @()
303
+ $feedOk = $false
304
+ if ($feedUri) {
305
+ $fi = $null
306
+ try { $fi = Get-FeedInfo $feedUri } catch { Log ('feed unreachable, skipping -AppInstallerFile: ' + $feedUri + ' : ' + $_.Exception.Message); $why += ('feed unreachable: ' + $_.Exception.Message) }
307
+ if ($fi) {
308
+ $bad = Test-Identity $expect $fi.name $fi.publisher
309
+ if (-not $bad -and $fi.uri -and -not (Test-AllowedUri $fi.uri)) { $bad = 'feed msix uri is not https (or http to loopback): ' + $fi.uri }
310
+ if ($bad) { Log ('REFUSED feed ' + $feedUri + ': ' + $bad); $why += ('feed refused: ' + $bad) }
311
+ else {
312
+ $feedOk = $true
313
+ if ($fi.uri) { $msix = $fi.uri } # newer than the one recorded
314
+ Log ('Add-AppxPackage -AppInstallerFile ' + $feedUri + ' (declares ' + $fi.version + ')')
315
+ try { Add-AppxPackage -AppInstallerFile $feedUri -ErrorAction Stop } catch { Log ('-AppInstallerFile failed: ' + $_.Exception.Message + ' | ' + $_.FullyQualifiedErrorId); $why += ('-AppInstallerFile failed: ' + $_.Exception.Message) }
316
+ if (Test-Healthy $family) {
317
+ $now = @(Get-Family $family) | Select-Object -First 1
318
+ $bad = Test-Identity $expect ([string]$now.Name) ([string]$now.Publisher)
319
+ if ($bad) { Log ('FAILED: identity mismatch after repair via feed: ' + $bad); $why += ('identity after repair: ' + $bad) }
320
+ else { Clear-Retry $family; Finish 'repaired' 0 ('REPAIRED via feed: ' + (Get-StateText $family)) }
321
+ }
322
+ }
323
+ }
324
+ }
325
+ if ($msix) {
326
+ $tmpMsix = Join-Path $env:TEMP ($family + '-repair-' + [guid]::NewGuid().ToString('N') + '.msix')
327
+ $got = $false
328
+ try { Log ('downloading ' + $msix); Save-Msix $msix $tmpMsix; $got = $true } catch { Log ('msix unreachable, skipping -Path: ' + $msix + ' : ' + $_.Exception.Message); $why += ('msix unreachable: ' + $_.Exception.Message) }
329
+ if ($got) {
330
+ $mi = $null
331
+ try { $mi = Get-MsixIdentity $tmpMsix } catch { Log ('REFUSED msix: corrupt or unreadable package: ' + $_.Exception.Message); $why += ('msix corrupt: ' + $_.Exception.Message) }
332
+ if ($mi) {
333
+ $bad = Test-Identity $expect $mi.name $mi.publisher
334
+ if ($bad) { Log ('REFUSED msix ' + $msix + ': ' + $bad); $why += ('msix refused: ' + $bad) }
335
+ else {
336
+ Log ('Add-AppxPackage -Path ' + $msix + ' (' + $mi.kind + ' ' + $mi.version + ')')
337
+ try { Add-AppxPackage -Path $tmpMsix -ErrorAction Stop } catch { Log ('-Path failed: ' + $_.Exception.Message + ' | ' + $_.FullyQualifiedErrorId); $why += ('-Path failed: ' + $_.Exception.Message) }
338
+ if (Test-Healthy $family) {
339
+ $now = @(Get-Family $family) | Select-Object -First 1
340
+ $bad = Test-Identity $expect ([string]$now.Name) ([string]$now.Publisher)
341
+ if ($bad) { Log ('FAILED: identity mismatch after repair via msix: ' + $bad); $why += ('identity after repair: ' + $bad) }
342
+ else { Clear-Retry $family; Finish 'repaired' 0 ('REPAIRED via msix uri: ' + (Get-StateText $family)) }
343
+ }
344
+ }
345
+ }
346
+ }
347
+ } else { $why += 'no msix uri recorded' }
348
+ $detail = 'FAILED: package still ' + (Get-StateText $family) + '. Reasons: ' + ($why -join ' | ')
349
+ Set-Retry $family 'repair failed'
350
+ Write-Result 'failed' 1 $detail
351
+ Log ($detail + '. A retry is scheduled (see retry.json); manual: Settings > Apps > Advanced options > Repair.')
352
+ exit 1
353
+ } finally {
354
+ if ($tmpMsix) { try { Remove-Item -LiteralPath $tmpMsix -Force -ErrorAction SilentlyContinue } catch { } }
355
+ try { $mutex.ReleaseMutex() } catch { }
356
+ }
@@ -0,0 +1,211 @@
1
+ // Default signer-subject reader for .msix / .msixbundle.
2
+ // PowerShell Get-AuthenticodeSignature returns nothing for MSIX on hosted Windows runners, so the subject
3
+ // is read from the package's own AppxSignature.p7x: a "PKCX" magic followed by a PKCS#7 SignedData.
4
+ // Pure Node (node:zlib + node:crypto X509Certificate + a small DER walker); no dependency, any host.
5
+ // The certificate chosen is the one named by SignerInfo issuerAndSerialNumber (or subjectKeyIdentifier),
6
+ // never merely the first certificate. Every failure throws MSIX_PUBLISHER_MISMATCH (fail closed).
7
+
8
+ import { X509Certificate } from "node:crypto";
9
+ import { closeSync, fstatSync, openSync, readSync } from "node:fs";
10
+ import { inflateRawSync } from "node:zlib";
11
+
12
+ const fail = (message) => Object.assign(new Error(message), { code: "MSIX_PUBLISHER_MISMATCH" });
13
+ const MAX_P7X = 4 * 1024 * 1024;
14
+ const SIGNED_DATA_OID = "1.2.840.113549.1.7.2";
15
+
16
+ // --- zip container -----------------------------------------------------------------------------
17
+ const u64 = (b, o) => Number(b.readBigUInt64LE(o));
18
+
19
+ function readAt(fd, position, length) {
20
+ const buffer = Buffer.alloc(length);
21
+ let done = 0;
22
+ while (done < length) {
23
+ const n = readSync(fd, buffer, done, length - done, position + done);
24
+ if (n === 0) break;
25
+ done += n;
26
+ }
27
+ return buffer.subarray(0, done);
28
+ }
29
+
30
+ /** Return the bytes of the named entry of a zip file, or null when absent. */
31
+ export function readZipEntry(file, wanted, { maxBytes = MAX_P7X } = {}) {
32
+ const fd = openSync(file, "r");
33
+ try {
34
+ const size = fstatSync(fd).size;
35
+ const tailLength = Math.min(size, 22 + 65535);
36
+ const tail = readAt(fd, size - tailLength, tailLength);
37
+ let entries; let dirSize; let dirOffset; let found = false;
38
+ for (let i = tail.length - 22; i >= 0; i -= 1) {
39
+ if (tail.readUInt32LE(i) !== 0x06054b50) continue;
40
+ if (i + 22 + tail.readUInt16LE(i + 20) > tail.length) continue;
41
+ entries = tail.readUInt16LE(i + 10); dirSize = tail.readUInt32LE(i + 12); dirOffset = tail.readUInt32LE(i + 16);
42
+ if (entries === 0xffff || dirSize === 0xffffffff || dirOffset === 0xffffffff) {
43
+ const at = size - tailLength + i;
44
+ const loc = at >= 20 ? readAt(fd, at - 20, 20) : Buffer.alloc(0);
45
+ if (loc.length < 20 || loc.readUInt32LE(0) !== 0x07064b50) throw new Error("zip64 locator missing");
46
+ const rec = readAt(fd, u64(loc, 8), 56);
47
+ if (rec.length < 56 || rec.readUInt32LE(0) !== 0x06064b50) throw new Error("zip64 end record missing");
48
+ entries = u64(rec, 32); dirSize = u64(rec, 40); dirOffset = u64(rec, 48);
49
+ }
50
+ found = true;
51
+ break;
52
+ }
53
+ if (!found) throw new Error("not a zip container (end of central directory not found)");
54
+ if (dirSize > 256 * 1024 * 1024 || dirOffset + dirSize > size) throw new Error("central directory out of bounds");
55
+ const dir = readAt(fd, dirOffset, dirSize);
56
+ if (dir.length !== dirSize) throw new Error("central directory truncated");
57
+ let at = 0;
58
+ for (let n = 0; n < entries; n += 1) {
59
+ if (at + 46 > dir.length || dir.readUInt32LE(at) !== 0x02014b50) throw new Error("bad central directory entry");
60
+ const flags = dir.readUInt16LE(at + 8); const method = dir.readUInt16LE(at + 10);
61
+ let compressed = dir.readUInt32LE(at + 20); let uncompressed = dir.readUInt32LE(at + 24);
62
+ const nameLen = dir.readUInt16LE(at + 28); const extraLen = dir.readUInt16LE(at + 30); const commentLen = dir.readUInt16LE(at + 32);
63
+ let local = dir.readUInt32LE(at + 42);
64
+ const end = at + 46 + nameLen + extraLen + commentLen;
65
+ if (end > dir.length) throw new Error("central directory entry out of bounds");
66
+ const name = dir.subarray(at + 46, at + 46 + nameLen).toString("utf8");
67
+ if (name === wanted) {
68
+ const extra = dir.subarray(at + 46 + nameLen, at + 46 + nameLen + extraLen);
69
+ for (let e = 0; e + 4 <= extra.length; e += 4 + extra.readUInt16LE(e + 2)) {
70
+ if (extra.readUInt16LE(e) !== 0x0001) continue;
71
+ let p = e + 4;
72
+ if (uncompressed === 0xffffffff) { uncompressed = u64(extra, p); p += 8; }
73
+ if (compressed === 0xffffffff) { compressed = u64(extra, p); p += 8; }
74
+ if (local === 0xffffffff) local = u64(extra, p);
75
+ }
76
+ if (flags & 1) throw new Error(`${wanted} is encrypted`);
77
+ if (uncompressed > maxBytes || compressed > maxBytes) throw new Error(`${wanted} larger than ${maxBytes} bytes`);
78
+ const lh = readAt(fd, local, 30);
79
+ if (lh.length < 30 || lh.readUInt32LE(0) !== 0x04034b50) throw new Error("bad local header");
80
+ const start = local + 30 + lh.readUInt16LE(26) + lh.readUInt16LE(28);
81
+ if (start + compressed > size) throw new Error("entry data out of bounds");
82
+ const raw = readAt(fd, start, compressed);
83
+ let data;
84
+ if (method === 0) data = raw;
85
+ else if (method === 8) data = inflateRawSync(raw, { maxOutputLength: maxBytes });
86
+ else throw new Error(`unsupported zip method ${method}`);
87
+ if (data.length !== uncompressed) throw new Error(`${wanted} length differs from its directory entry`);
88
+ return data;
89
+ }
90
+ at = end;
91
+ }
92
+ return null;
93
+ } finally { closeSync(fd); }
94
+ }
95
+
96
+ // --- DER ---------------------------------------------------------------------------------------
97
+ /** Read one TLV at `at`; returns { tag, start (of header), body (start), end }. */
98
+ function tlv(buf, at, limit = buf.length) {
99
+ if (at + 2 > limit) throw new Error("DER truncated");
100
+ const tag = buf[at]; let len = buf[at + 1]; let body = at + 2;
101
+ if ((tag & 0x1f) === 0x1f) throw new Error("DER high tag numbers unsupported");
102
+ if (len & 0x80) {
103
+ const n = len & 0x7f;
104
+ if (n === 0) return indefinite(buf, tag, at, limit); // BER indefinite length
105
+ if (n > 4 || body + n > limit) throw new Error("DER bad length");
106
+ len = 0;
107
+ for (let i = 0; i < n; i += 1) len = len * 256 + buf[body + i];
108
+ body += n;
109
+ }
110
+ if (body + len > limit) throw new Error("DER length exceeds container");
111
+ return { tag, start: at, body, end: body + len };
112
+ }
113
+
114
+ // BER indefinite-length constructed value: children until 00 00.
115
+ function indefinite(buf, tag, at, limit) {
116
+ if (!(tag & 0x20)) throw new Error("indefinite length on primitive");
117
+ const body = at + 2; let p = body;
118
+ while (true) {
119
+ if (p + 2 > limit) throw new Error("BER unterminated");
120
+ if (buf[p] === 0 && buf[p + 1] === 0) return { tag, start: at, body, end: p, tail: p + 2 };
121
+ p = next(tlv(buf, p, limit));
122
+ }
123
+ }
124
+ const next = (t) => t.tail ?? t.end;
125
+ const children = (buf, t) => {
126
+ const out = []; let p = t.body;
127
+ while (p < t.end) { const c = tlv(buf, p, t.end); out.push(c); p = next(c); }
128
+ return out;
129
+ };
130
+ const whole = (buf, t) => buf.subarray(t.start, next(t));
131
+ const oid = (buf, t) => {
132
+ const b = buf.subarray(t.body, t.end); const parts = [Math.floor(b[0] / 40), b[0] % 40]; let v = 0;
133
+ for (let i = 1; i < b.length; i += 1) { v = v * 128 + (b[i] & 0x7f); if (!(b[i] & 0x80)) { parts.push(v); v = 0; } }
134
+ return parts.join(".");
135
+ };
136
+ const intHex = (buf, t) => { let h = buf.subarray(t.body, t.end).toString("hex").replace(/^(00)+(?=.)/, ""); return h.toLowerCase(); };
137
+
138
+ /** Parse PKCS#7 SignedData; return { certificates: [Buffer], signer: { issuer: Buffer, serial: string } | { ski: string } }. */
139
+ export function parseSignedData(der) {
140
+ const ci = tlv(der, 0);
141
+ if (ci.tag !== 0x30) throw new Error("ContentInfo is not a SEQUENCE");
142
+ const [type, content] = children(der, ci);
143
+ if (!type || type.tag !== 0x06 || oid(der, type) !== SIGNED_DATA_OID) throw new Error("not a PKCS#7 SignedData");
144
+ if (!content || content.tag !== 0xa0) throw new Error("SignedData content missing");
145
+ const sd = children(der, content)[0];
146
+ if (!sd || sd.tag !== 0x30) throw new Error("SignedData is not a SEQUENCE");
147
+ const parts = children(der, sd);
148
+ const certsSet = parts.find((p) => p.tag === 0xa0);
149
+ const signerSet = parts.filter((p) => p.tag === 0x31).at(-1);
150
+ if (!certsSet) throw new Error("SignedData carries no certificates");
151
+ if (!signerSet) throw new Error("SignedData carries no SignerInfos");
152
+ const certificates = children(der, certsSet).filter((c) => c.tag === 0x30).map((c) => Buffer.from(whole(der, c)));
153
+ const infos = children(der, signerSet);
154
+ if (infos.length !== 1) throw new Error(`expected exactly one SignerInfo, found ${infos.length}`);
155
+ const [, sid] = children(der, infos[0]);
156
+ if (!sid) throw new Error("SignerInfo has no signer identifier");
157
+ let signer;
158
+ if (sid.tag === 0x30) {
159
+ const [issuer, serial] = children(der, sid);
160
+ signer = { issuer: Buffer.from(whole(der, issuer)), serial: intHex(der, serial) };
161
+ } else if (sid.tag === 0x80) signer = { ski: Buffer.from(der.subarray(sid.body, sid.end)).toString("hex") };
162
+ else throw new Error("unsupported SignerInfo signer identifier");
163
+ return { certificates, signer };
164
+ }
165
+
166
+ /** TBSCertificate issuer (DER) and serial (hex) of a certificate. */
167
+ function certIdentity(cert) {
168
+ const [tbs] = children(cert, tlv(cert, 0));
169
+ const f = children(cert, tbs);
170
+ const i = f[0].tag === 0xa0 ? 1 : 0; // optional [0] version
171
+ return { serial: intHex(cert, f[i]), issuer: Buffer.from(whole(cert, f[i + 2])) };
172
+ }
173
+
174
+ export function signerCertificate(parsed) {
175
+ const match = parsed.certificates.filter((c) => {
176
+ try {
177
+ if (parsed.signer.ski) {
178
+ const x = new X509Certificate(c);
179
+ return x.raw && (x.subjectKeyIdentifier ?? "").replace(/:/g, "").toLowerCase() === parsed.signer.ski;
180
+ }
181
+ const id = certIdentity(c);
182
+ return id.serial === parsed.signer.serial && id.issuer.equals(parsed.signer.issuer);
183
+ } catch { return false; }
184
+ });
185
+ if (match.length !== 1) throw new Error(`signer certificate not found in the signature (${match.length} matches)`);
186
+ return new X509Certificate(match[0]);
187
+ }
188
+
189
+ // --- subject rendering ---------------------------------------------------------------------------
190
+ const needsQuote = (v) => /[,+"\\<>;=\r\n]/.test(v) || /^[ #]|\s$/.test(v);
191
+ /** Node renders subject as "K=V" lines; join as an RFC 2253-style display string like .NET Subject ("CN=a, O=b"). */
192
+ export function renderSubject(x509) {
193
+ return String(x509.subject).split("\n").filter(Boolean).map((line) => {
194
+ const at = line.indexOf("=");
195
+ const key = line.slice(0, at); const value = line.slice(at + 1);
196
+ return `${key}=${needsQuote(value) ? `"${value.replace(/"/g, '""')}"` : value}`;
197
+ }).join(", ");
198
+ }
199
+
200
+ /** Read the signer certificate subject of a signed .msix / .msixbundle from AppxSignature.p7x. Throws MSIX_PUBLISHER_MISMATCH. */
201
+ export function readMsixSignerSubject(file) {
202
+ let p7x;
203
+ try { p7x = readZipEntry(file, "AppxSignature.p7x"); } catch (error) { throw fail(`could not read the signer certificate subject: ${error.message}`); }
204
+ if (!p7x) throw fail("could not read the signer certificate subject: AppxSignature.p7x is missing (package is unsigned)");
205
+ try {
206
+ if (p7x.length < 5 || p7x.subarray(0, 4).toString("latin1") !== "PKCX") throw new Error("AppxSignature.p7x lacks the PKCX header");
207
+ return renderSubject(signerCertificate(parseSignedData(p7x.subarray(4))));
208
+ } catch (error) {
209
+ throw fail(`could not read the signer certificate subject: AppxSignature.p7x unparsable (${error.message})`);
210
+ }
211
+ }
@@ -3,16 +3,15 @@
3
3
  "newCapabilityApprovalFlag": "--approve-new-capabilities",
4
4
  "approvalScope": "Local catalog generation only; installation still requires protected authorization. This flag cannot approve owner changes.",
5
5
  "ownerFamilies": [
6
- { "capability": "agent-room", "packages": ["agent-room-core", "@rightkit/agent-room-mcp"], "reason": "Native room core & MCP adapter share room contract" },
7
6
  { "capability": "licensing", "packages": ["rightkit-license", "@rightkit/license"], "reason": "Native entitlement enforcement & renderer/network client have separate roles" },
8
7
  { "capability": "structured-logging", "packages": ["rightkit-logs", "@rightkit/logs"], "reason": "Native JSONL writer & typed renderer invoke adapter" },
9
- { "capability": "text-to-speech", "packages": ["rightkit-qts", "rightkit-qts-ggml", "rightkit-qts-ggml-sys", "rightkit-tts"], "reason": "Speech facade & its QTS inference, safe-wrapper & FFI layers" },
10
- { "capability": "os-credential-store", "packages": ["rightkit-secrets", "RightKitSwift"], "reason": "Cross-platform Rust & Apple-native Swift credential APIs" },
11
- { "capability": "desktop-shell", "packages": ["rightkit-shell", "@rightkit/shell"], "reason": "Native Tauri shell & typed renderer bridge" },
12
- { "capability": "tauri-host-adapters", "packages": ["rightkit-tauri", "@rightkit/tauri"], "reason": "Native license/device services & renderer command adapters" }
8
+ { "capability": "text-to-speech", "packages": ["rightkit-tts-qwen", "rightkit-tts-qwen-sys", "rightkit-tts"], "reason": "Speech facade & its QTS inference, safe-wrapper & FFI layers" },
9
+ { "capability": "os-credential-store", "packages": ["rightkit-credentials", "RightKitSwift"], "reason": "Cross-platform Rust & Apple-native Swift credential APIs" },
10
+ { "capability": "desktop-shell", "packages": ["rightkit-tauri-shell", "@rightkit/tauri-shell"], "reason": "Native Tauri shell & typed renderer bridge" },
11
+ { "capability": "tauri-host-adapters", "packages": ["rightkit-device", "@rightkit/tauri"], "reason": "Native license/device services & renderer command adapters" }
13
12
  ],
14
13
  "testOwners": [
15
- { "path": "crates/rightkit-control/examples/control-test-app/Cargo.toml", "package": "control-test-app", "reason": "Unpublished desktop-control test fixture" },
16
- { "path": "crates/rightkit-shell/example-app/Cargo.toml", "package": "shell-example-app", "reason": "Unpublished standalone shell demonstration & test host" }
14
+ { "path": "crates/rightkit-qa-driver/examples/control-test-app/Cargo.toml", "package": "control-test-app", "reason": "Unpublished desktop-control test fixture" },
15
+ { "path": "crates/rightkit-tauri-shell/example-app/Cargo.toml", "package": "shell-example-app", "reason": "Unpublished standalone shell demonstration & test host" }
17
16
  ]
18
17
  }