fix(windows): defer missing tray task during trust reset

This commit is contained in:
lussierj committed 2026-10-14 13:50:36 +00:00
1 parent 97a0139e3d
commit 2d89e80410
3 files changed
+137

No files matched your search

+14
View File
@@ -197,6 +197,20 @@ and other runtime data. Existing Scheduled Task definitions are authoritative
and are not rewritten. Missing tasks are created for a fresh or incomplete and are not rewritten. Missing tasks are created for a fresh or incomplete
installation without hard-coded user names. installation without hard-coded user names.
The per-user `MinerOpsAgentTray` task is optional UI and is not credential or
core Agent authority. During an explicit filesystem trust reset, runtime
publication may run noninteractively as `NT AUTHORITY\SYSTEM`, where no safe
interactive user principal can be inferred. The installer still publishes
`MinerOpsAgentTray.ps1`, but deliberately defers creation of a missing tray task
until a later interactive maintenance install. It never creates the tray task
as SYSTEM or invents a username. An existing tray task remains authoritative
and is preserved unchanged rather than silently repaired under SYSTEM; inspect
and remediate a suspected malformed definition separately. The core
`MinerOpsAgent` task remains mandatory and strictly verified by credential
recovery. `MinerOpsApi3Enrichment` and `MinerOpsPoolPoller` are non-interactive
background tasks with explicit SYSTEM principals, so missing instances continue
to be created during trust-reset publication.
If MinerOpsAgent was running before an upgrade, the installer stops only that If MinerOpsAgent was running before an upgrade, the installer stops only that
Scheduled Task, waits for it to stop, replaces the managed files, and starts it Scheduled Task, waits for it to stop, replaces the managed files, and starts it
again. A previously stopped Agent remains stopped. Before replacing runtime again. A previously stopped Agent remains stopped. Before replacing runtime
+27
View File
@@ -953,6 +953,19 @@ function Get-MinerOpsScheduledTask {
return $tasks[0] return $tasks[0]
} }
function Test-MinerOpsInstallerShouldCreateMissingTask {
param(
[Parameter(Mandatory)]$Definition,
[Parameter(Mandatory)][bool]$TrustResetMode
)
if (-not $TrustResetMode) { return $true }
if ($null -eq $Definition.CreateWhenMissingDuringTrustReset) {
throw "Scheduled Task trust-reset creation policy is missing."
}
return [bool]$Definition.CreateWhenMissingDuringTrustReset
}
function Stop-MinerOpsScheduledTaskForUpgrade { function Stop-MinerOpsScheduledTaskForUpgrade {
param( param(
[Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$Name,
@@ -1101,22 +1114,26 @@ try {
Name = "MinerOpsAgent"; Script = "MinerOpsAgentService.ps1" Name = "MinerOpsAgent"; Script = "MinerOpsAgentService.ps1"
Trigger = $startup; Principal = $system; Settings = $agentSettings Trigger = $startup; Principal = $system; Settings = $agentSettings
ExtraArguments = "" ExtraArguments = ""
CreateWhenMissingDuringTrustReset = $true
}, },
@{ @{
Name = "MinerOpsAgentTray"; Script = "MinerOpsAgentTray.ps1" Name = "MinerOpsAgentTray"; Script = "MinerOpsAgentTray.ps1"
Trigger = (New-ScheduledTaskTrigger -AtLogOn); Principal = $null Trigger = (New-ScheduledTaskTrigger -AtLogOn); Principal = $null
Settings = $commonSettings; ExtraArguments = "-WindowStyle Hidden" Settings = $commonSettings; ExtraArguments = "-WindowStyle Hidden"
CreateWhenMissingDuringTrustReset = $false
}, },
@{ @{
Name = "MinerOpsApi3Enrichment" Name = "MinerOpsApi3Enrichment"
Script = "MinerOpsApi3EnrichmentPoller.ps1" Script = "MinerOpsApi3EnrichmentPoller.ps1"
Trigger = $fiveMinutes; Principal = $system; Settings = $commonSettings Trigger = $fiveMinutes; Principal = $system; Settings = $commonSettings
ExtraArguments = "-Limit 0 -TimeoutMs 8000" ExtraArguments = "-Limit 0 -TimeoutMs 8000"
CreateWhenMissingDuringTrustReset = $true
}, },
@{ @{
Name = "MinerOpsPoolPoller"; Script = "MinerOpsPoolPollerTask.ps1" Name = "MinerOpsPoolPoller"; Script = "MinerOpsPoolPollerTask.ps1"
Trigger = $fiveMinutes; Principal = $system; Settings = $commonSettings Trigger = $fiveMinutes; Principal = $system; Settings = $commonSettings
ExtraArguments = "" ExtraArguments = ""
CreateWhenMissingDuringTrustReset = $true
} }
) )
foreach ($definition in $definitions) { foreach ($definition in $definitions) {
@@ -1129,6 +1146,16 @@ try {
continue continue
} }
if (-not (Test-MinerOpsInstallerShouldCreateMissingTask `
-Definition $definition -TrustResetMode $trustResetMode)) {
# Trust-reset publication can run noninteractively as SYSTEM. A
# missing per-user tray has no safe interactive principal in that
# context. Its runtime file is already published; task creation is
# intentionally deferred to a later interactive maintenance run.
Write-Host "Deferring missing per-user Scheduled Task $($definition.Name) until an interactive install."
continue
}
$arguments = '-NoProfile -ExecutionPolicy Bypass -File "{0}"' -f ` $arguments = '-NoProfile -ExecutionPolicy Bypass -File "{0}"' -f `
(Join-Path $Target $definition.Script) (Join-Path $Target $definition.Script)
if ($definition.ExtraArguments) { if ($definition.ExtraArguments) {
@@ -37,6 +37,10 @@ Assert-SourceContains '\$existingTask\s*=\s*Get-MinerOpsScheduledTask' `
"Existing task preservation guard is missing." "Existing task preservation guard is missing."
Assert-SourceContains 'Preserving existing Scheduled Task' ` Assert-SourceContains 'Preserving existing Scheduled Task' `
"Existing task preservation is not explicit." "Existing task preservation is not explicit."
Assert-SourceContains 'Test-MinerOpsInstallerShouldCreateMissingTask' `
"Missing-task trust-reset policy helper is absent."
Assert-SourceContains 'Deferring missing per-user Scheduled Task' `
"Missing per-user task deferral is not explicit."
Assert-SourceContains 'ExtraArguments\s*=\s*"-Limit 0 -TimeoutMs 8000"' ` Assert-SourceContains 'ExtraArguments\s*=\s*"-Limit 0 -TimeoutMs 8000"' `
"Fresh enrichment task arguments are missing." "Fresh enrichment task arguments are missing."
Assert-SourceContains 'Stop-MinerOpsScheduledTaskForUpgrade' ` Assert-SourceContains 'Stop-MinerOpsScheduledTaskForUpgrade' `
@@ -52,6 +56,98 @@ Assert-SourceContains 'could not restart restored MinerOpsAgent' `
Assert-SourceContains 'Assert-MinerOpsInstallerTrustedAncestors' ` Assert-SourceContains 'Assert-MinerOpsInstallerTrustedAncestors' `
"Installer does not validate ancestor namespace protection." "Installer does not validate ancestor namespace protection."
$taskDefinitionsStart = $source.IndexOf('$definitions = @(')
$taskLoopStart = $source.IndexOf(
'foreach ($definition in $definitions)', $taskDefinitionsStart
)
if ($taskDefinitionsStart -lt 0 -or $taskLoopStart -le $taskDefinitionsStart) {
throw "Scheduled Task definitions or reconciliation loop were not found."
}
$taskDefinitions = $source.Substring(
$taskDefinitionsStart, $taskLoopStart - $taskDefinitionsStart
)
foreach ($policy in @(
@{ Name = "MinerOpsAgent"; Expected = '\$true' },
@{ Name = "MinerOpsAgentTray"; Expected = '\$false' },
@{ Name = "MinerOpsApi3Enrichment"; Expected = '\$true' },
@{ Name = "MinerOpsPoolPoller"; Expected = '\$true' }
)) {
$definition = [regex]::Match(
$taskDefinitions,
('(?s)@\{{.*?Name\s*=\s*"{0}".*?\}}' -f
[regex]::Escape($policy.Name))
).Value
if ([string]::IsNullOrWhiteSpace($definition) -or
$definition -notmatch (
'CreateWhenMissingDuringTrustReset\s*=\s*{0}' -f
$policy.Expected
)) {
throw "Scheduled Task $($policy.Name) has the wrong trust-reset creation policy."
}
}
$taskLoopEnd = $source.IndexOf(
'if ($mainTaskWasRunning', $taskLoopStart
)
if ($taskLoopEnd -le $taskLoopStart) {
throw "Scheduled Task reconciliation loop end was not found."
}
$taskLoop = $source.Substring($taskLoopStart, $taskLoopEnd - $taskLoopStart)
$preserveIndex = $taskLoop.IndexOf('$null -ne $existingTask')
$deferIndex = $taskLoop.IndexOf(
'Test-MinerOpsInstallerShouldCreateMissingTask'
)
$actionIndex = $taskLoop.IndexOf('New-ScheduledTaskAction')
if (
$preserveIndex -lt 0 -or
$deferIndex -le $preserveIndex -or
$actionIndex -le $deferIndex
) {
throw "Existing-task preservation and missing-task deferral are ordered unsafely."
}
if ($taskLoop -match '80070534|No mapping between account names') {
throw "Missing per-user task handling must be policy-based, not HRESULT-based."
}
$taskPolicyFunction = [regex]::Match(
$source,
'(?s)function Test-MinerOpsInstallerShouldCreateMissingTask \{.*?\n\}'
).Value
if ([string]::IsNullOrWhiteSpace($taskPolicyFunction)) {
throw "Missing-task trust-reset policy function source was not found."
}
Invoke-Expression $taskPolicyFunction
$trayPolicy = [pscustomobject]@{
CreateWhenMissingDuringTrustReset = $false
}
$systemTaskPolicy = [pscustomobject]@{
CreateWhenMissingDuringTrustReset = $true
}
if (Test-MinerOpsInstallerShouldCreateMissingTask `
-Definition $trayPolicy -TrustResetMode $true) {
throw "Trust reset would create a missing per-user tray task."
}
if (-not (Test-MinerOpsInstallerShouldCreateMissingTask `
-Definition $trayPolicy -TrustResetMode $false)) {
throw "Normal interactive installation would not repair a missing tray task."
}
if (-not (Test-MinerOpsInstallerShouldCreateMissingTask `
-Definition $systemTaskPolicy -TrustResetMode $true)) {
throw "Trust reset would skip a missing explicit-SYSTEM task."
}
$missingPolicyRejected = $false
try {
[void](Test-MinerOpsInstallerShouldCreateMissingTask `
-Definition ([pscustomobject]@{}) -TrustResetMode $true)
}
catch {
$missingPolicyRejected = $_.Exception.Message -match
'trust-reset creation policy is missing'
}
if (-not $missingPolicyRejected) {
throw "Trust reset accepted a task definition without explicit policy."
}
foreach ($functionName in @( foreach ($functionName in @(
"Assert-MinerOpsInstallerNoReparsePath", "Assert-MinerOpsInstallerNoReparsePath",
"Assert-MinerOpsInstallerTrustedAncestors" "Assert-MinerOpsInstallerTrustedAncestors"