security: contain physical miner commands in Stage A

Disable physical control through legacy HTTP/M30 producers, typed admission,
queue dispatch and upgraded Windows executors. Restrict diagnostic transports
to exact read-only operations, with ordinal PowerShell validation before sockets.

Retire unsupported pending/claimed work with atomic, idempotent security audits.
Preserve prior delivery evidence and report unknown outcomes after a prior claim,
including explicitly repaired pending rows. Include containment, cancellation,
concurrency and malformed-payload regressions and the old-Agent cutover runbook.

Validation: independent final application-security revalidation PASS. Two unchanged
baseline source-assertion failures are documented as exceptions. Production release
still requires native Windows validation and verified fencing/stop/drain/upgrade;
server cancellation alone cannot revoke a cached command held by an old Agent.
This commit is contained in:
lussierj committed 2026-10-15 20:39:45 +00:00
1 parent 2d89e80410
commit e898f283ab
28 files changed
+2377 -2379

No files matched your search

+19 -1
View File
@@ -23,6 +23,24 @@ function Invoke-MinerCommandRaw {
[int]$TimeoutMs = 2500
)
# This transport accepts only the exact queries used by this enrichment script.
# Validate before creating a socket; callers cannot reinterpret a write as a read.
$readCommands = @("summary", "devs", "edevs", "stats", "pools", "devdetails",
"version", "get_version", "get_miner_info")
# PowerShell containment operators use culture comparisons, even with -c.
# Match ordinally and serialize the canonical literal; never trim/normalize.
$approvedCommand = $null
foreach ($candidate in $readCommands) {
if ([string]::Equals($candidate, $Command, [StringComparison]::Ordinal)) {
$approvedCommand = $candidate
break
}
}
if ($Port -ne 4028 -or $null -eq $approvedCommand) {
throw "unsupported_diagnostic_operation_stage_a"
}
$requestJson = @{ cmd = $approvedCommand } | ConvertTo-Json -Compress
$client = $null
try {
@@ -38,7 +56,7 @@ function Invoke-MinerCommandRaw {
$client.SendTimeout = $TimeoutMs
$stream = $client.GetStream()
$payload = [System.Text.Encoding]::ASCII.GetBytes("{`"cmd`":`"$Command`"}`n")
$payload = [System.Text.Encoding]::ASCII.GetBytes($requestJson + "`n")
$stream.Write($payload, 0, $payload.Length)
try {
+9 -388
View File
@@ -743,61 +743,7 @@ function Invoke-MinerPlainCommand {
[int]$TimeoutMs = 2500
)
$client = $null
try {
$client = New-Object System.Net.Sockets.TcpClient
$async = $client.BeginConnect($IpAddress, $Port, $null, $null)
if (-not $async.AsyncWaitHandle.WaitOne($TimeoutMs)) {
throw "Connect timeout to $IpAddress`:$Port"
}
$client.EndConnect($async)
$client.ReceiveTimeout = $TimeoutMs
$client.SendTimeout = $TimeoutMs
$stream = $client.GetStream()
$payload = [System.Text.Encoding]::ASCII.GetBytes("{`"cmd`":`"$Command`"}`n")
$stream.Write($payload, 0, $payload.Length)
try {
$client.Client.Shutdown([System.Net.Sockets.SocketShutdown]::Send)
} catch {
}
$buffer = New-Object byte[] 65536
$chunks = New-Object System.Collections.ArrayList
$started = Get-Date
while (((Get-Date) - $started).TotalMilliseconds -lt $TimeoutMs) {
if (-not $stream.DataAvailable) {
Start-Sleep -Milliseconds 20
continue
}
$read = $stream.Read($buffer, 0, $buffer.Length)
if ($read -le 0) {
break
}
for ($i = 0; $i -lt $read; $i++) {
[void]$chunks.Add([byte]$buffer[$i])
}
if ($chunks.Count -gt 1048576) {
break
}
}
return [System.Text.Encoding]::UTF8.GetString([byte[]]$chunks.ToArray([byte])).Trim()
} finally {
if ($client -ne $null) {
$client.Close()
}
}
throw "physical_control_disabled_stage_a"
}
function Get-MinerOpsApi3Credential {
@@ -894,61 +840,7 @@ function Invoke-MinerOpsApi3Payload {
[int]$TimeoutMs = 10000
)
$client = $null
$started = Get-Date
try {
$client = New-Object System.Net.Sockets.TcpClient
$async = $client.BeginConnect($IpAddress, $Port, $null, $null)
if (-not $async.AsyncWaitHandle.WaitOne($TimeoutMs)) {
throw "API3 connect timeout to $IpAddress`:$Port"
}
$client.EndConnect($async)
$client.ReceiveTimeout = $TimeoutMs
$client.SendTimeout = $TimeoutMs
$json = $Payload | ConvertTo-Json -Depth 20 -Compress
$requestBytes = [System.Text.Encoding]::UTF8.GetBytes($json)
$lengthBytes = [System.BitConverter]::GetBytes([UInt32]$requestBytes.Length)
$stream = $client.GetStream()
$stream.Write($lengthBytes, 0, $lengthBytes.Length)
$stream.Write($requestBytes, 0, $requestBytes.Length)
$responseLengthBytes = Read-MinerOpsExactBytes -Stream $stream -Length 4 -TimeoutMs $TimeoutMs
$responseLength = [System.BitConverter]::ToUInt32($responseLengthBytes, 0)
if ($responseLength -le 0 -or $responseLength -gt 1048576) {
throw "API3 response length looked invalid: $responseLength"
}
$responseBytes = Read-MinerOpsExactBytes -Stream $stream -Length ([int]$responseLength) -TimeoutMs $TimeoutMs
$responseText = [System.Text.Encoding]::UTF8.GetString($responseBytes)
$responseJson = $responseText | ConvertFrom-Json
return @{
sent = $true
protocol = "whatsminer_api3"
target_ip = $IpAddress
target_port = $Port
duration_seconds = [math]::Round(((Get-Date) - $started).TotalSeconds, 3)
request = @{
cmd = $Payload.cmd
param = $Payload.param
account = $Payload.account
has_token = [bool]$Payload.token
}
response = $responseJson
}
} finally {
if ($client -ne $null) {
$client.Close()
}
}
throw "physical_control_disabled_stage_a"
}
function Get-MinerOpsApi3Salt {
@@ -989,43 +881,7 @@ function Invoke-MinerOpsApi3Service {
[ValidateSet("start", "stop", "restart")]
[string]$ServiceAction
)
$account = Get-MinerOpsApi3Credential -Name "api3_account" -DefaultValue ""
$password = Get-MinerOpsApi3Credential -Name "api3_password" -DefaultValue ""
$portRaw = Get-MinerOpsApi3Credential -Name "api3_port" -DefaultValue "4433"
try {
$port = [int]$portRaw
} catch {
$port = 4433
}
$command = "set.miner.service"
$saltInfo = Get-MinerOpsApi3Salt -IpAddress $IpAddress -Port $port
$salt = [string]$saltInfo.salt
$timestamp = [int64][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$token = Get-MinerOpsApi3Token -Command $command -Password $password -Salt $salt -Timestamp $timestamp
$payload = @{
cmd = $command
param = $ServiceAction
ts = $timestamp
token = $token
account = $account
}
$result = Invoke-MinerOpsApi3Payload -IpAddress $IpAddress -Port $port -Payload $payload
$result["salt_response"] = $saltInfo.salt_response
$result["api3_command"] = $command
$result["api3_param"] = $ServiceAction
$result["api3_account"] = $account
if ([int]$result.response.code -ne 0) {
throw "API3 $command $ServiceAction failed: $($result.response | ConvertTo-Json -Compress)"
}
return $result
throw "physical_control_disabled_stage_a"
}
@@ -1054,45 +910,7 @@ function Invoke-MinerOpsApi3AuthenticatedCommand {
[object]$Param = $null
)
$account = Get-MinerOpsApi3Credential -Name "api3_account" -DefaultValue ""
$password = Get-MinerOpsApi3Credential -Name "api3_password" -DefaultValue ""
$portRaw = Get-MinerOpsApi3Credential -Name "api3_port" -DefaultValue "4433"
try {
$port = [int]$portRaw
} catch {
$port = 4433
}
$saltInfo = Get-MinerOpsApi3Salt -IpAddress $IpAddress -Port $port
$salt = [string]$saltInfo.salt
$timestamp = [int64][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$token = Get-MinerOpsApi3Token -Command $Command -Password $password -Salt $salt -Timestamp $timestamp
$payload = @{
cmd = $Command
ts = $timestamp
token = $token
account = $account
}
if ($null -ne $Param) {
$payload["param"] = $Param
}
$result = Invoke-MinerOpsApi3Payload -IpAddress $IpAddress -Port $port -Payload $payload
$result["salt_response"] = $saltInfo.salt_response
$result["api3_command"] = $Command
$result["api3_param"] = $Param
$result["api3_account"] = $account
if ([int]$result.response.code -ne 0) {
throw "API3 $Command failed: $($result.response | ConvertTo-Json -Compress)"
}
return $result
throw "physical_control_disabled_stage_a"
}
function Get-MinerOpsPayloadValue {
@@ -1129,113 +947,14 @@ function Invoke-MinerOpsLegacy4028Command {
[int] $Port = 4028,
[int] $TimeoutMs = 7000
)
$normalizedIp = Normalize-MinerOpsIpAddress -IpAddress $IpAddress
$client = New-Object System.Net.Sockets.TcpClient
$connectResult = $client.BeginConnect($normalizedIp, $Port, $null, $null)
if (-not $connectResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) {
try { $client.Close() } catch {}
throw "Legacy 4028 connection timed out to $normalizedIp`:$Port"
}
$client.EndConnect($connectResult)
try {
$stream = $client.GetStream()
$stream.ReadTimeout = $TimeoutMs
$stream.WriteTimeout = $TimeoutMs
$bytes = [System.Text.Encoding]::ASCII.GetBytes($CommandJson)
$stream.Write($bytes, 0, $bytes.Length)
$stream.Flush()
Start-Sleep -Milliseconds 500
$buffer = New-Object byte[] 65535
$builder = New-Object System.Text.StringBuilder
do {
$read = $stream.Read($buffer, 0, $buffer.Length)
if ($read -gt 0) {
[void]$builder.Append([System.Text.Encoding]::ASCII.GetString($buffer, 0, $read))
}
} while ($stream.DataAvailable)
$rawResponse = $builder.ToString().Trim([char]0).Trim()
$parsedResponse = $null
try {
if ($rawResponse) {
$parsedResponse = $rawResponse | ConvertFrom-Json
}
} catch {
$parsedResponse = $null
}
return @{
success = $true
control_method = "legacy_4028"
port = $Port
request_json = $CommandJson
raw_response = $rawResponse
parsed_response = $parsedResponse
}
}
finally {
try { $stream.Close() } catch {}
try { $client.Close() } catch {}
}
throw "physical_control_disabled_stage_a"
}
function Invoke-MinerOpsM30Legacy4028Test {
param(
[Parameter(Mandatory=$true)] $Payload
)
if ($Payload -is [string]) {
$Payload = $Payload | ConvertFrom-Json
}
$ipAddress = Get-MinerOpsPayloadValue -Payload $Payload -Name "ip_address"
if (-not $ipAddress) {
$ipAddress = Get-MinerOpsPayloadValue -Payload $Payload -Name "current_ip"
}
if (-not $ipAddress) {
throw "Legacy 4028 test failed: no IP address in payload."
}
$legacyJson = Get-MinerOpsPayloadValue -Payload $Payload -Name "legacy_4028_json"
$legacyCommand = Get-MinerOpsPayloadValue -Payload $Payload -Name "legacy_4028_command"
if (-not $legacyJson) {
if (-not $legacyCommand) {
throw "Legacy 4028 test failed: no legacy command in payload."
}
$legacyJson = '{"command":"' + $legacyCommand + '"}'
}
Write-AgentLog "Running legacy 4028 M30 test against $ipAddress command=$legacyCommand"
$result = Invoke-MinerOpsLegacy4028Command -IpAddress $ipAddress -CommandJson $legacyJson -Port 4028
return @{
success = $true
action = "legacy_4028"
test_lab = "m30"
ip_address = $ipAddress
legacy_4028_command = $legacyCommand
legacy_4028_request = $legacyJson
legacy_4028_response = $result.raw_response
legacy_4028_parsed = $result.parsed_response
control_method = "legacy_4028"
}
throw "physical_control_disabled_stage_a"
}
function Run-MinerControlCommand {
@@ -1244,105 +963,7 @@ function Run-MinerControlCommand {
[Parameter(Mandatory=$true)]
[object]$Payload
)
# MinerOps M30 legacy 4028 test path
if ($Payload -is [string]) {
try {
$Payload = $Payload | ConvertFrom-Json
} catch {
}
}
if ($Payload -and (($Payload.control_method -eq "legacy_4028") -or ($Payload.action -eq "legacy_4028"))) {
return Invoke-MinerOpsM30Legacy4028Test -Payload $Payload
}
if ($Payload -is [string]) {
$Payload = $Payload | ConvertFrom-Json
}
if ($null -eq $Payload) {
throw "Miner control payload was empty."
}
$action = ([string]$Payload.action).ToLower()
$minerId = [string]$Payload.miner_id
$macAddress = [string]$Payload.mac_address
$dryRun = [bool]$Payload.dry_run
$ipAddress = ([string]$Payload.ip_address).Trim()
if ($ipAddress.Contains("/")) {
$ipAddress = $ipAddress.Split("/")[0]
}
if ([string]::IsNullOrWhiteSpace($ipAddress)) {
throw "Miner control payload did not include ip_address."
}
$serviceAction = ""
$api3Command = ""
$api3Param = $null
$performanceMode = ""
if ($action -eq "reboot") {
$serviceAction = "restart"
$api3Command = "set.miner.service"
} elseif ($action -eq "suspend") {
$serviceAction = "stop"
$api3Command = "set.miner.service"
} elseif ($action -eq "resume") {
$serviceAction = "start"
$api3Command = "set.miner.service"
} elseif ($action -eq "performance_mode") {
$performanceMode = ([string]$Payload.performance_mode).ToLower()
if ($performanceMode -in @("low", "normal", "high")) {
$api3Command = "set.miner.power_mode"
$api3Param = $performanceMode
} else {
throw "Invalid performance_mode: $performanceMode"
}
} else {
throw "Unsupported miner control action: $action"
}
Write-AgentLog "Miner control requested. Action=$action API3Command=$api3Command API3Service=$serviceAction PerformanceMode=$performanceMode IP=$ipAddress MAC=$macAddress DryRun=$dryRun"
if ($dryRun) {
return @{
ok = $true
dry_run = $true
action = $action
api3_command = $api3Command
api3_service_action = $serviceAction
performance_mode = $performanceMode
ip_address = $ipAddress
miner_id = $minerId
mac_address = $macAddress
message = "Dry run only. No miner command sent."
}
}
if ($action -eq "performance_mode") {
$api3Result = Invoke-MinerOpsApi3AuthenticatedCommand -IpAddress $ipAddress -Command $api3Command -Param $api3Param
} else {
$api3Result = Invoke-MinerOpsApi3Service -IpAddress $ipAddress -ServiceAction $serviceAction
}
return @{
ok = $true
dry_run = $false
action = $action
api3_command = $api3Command
api3_service_action = $serviceAction
performance_mode = $performanceMode
ip_address = $ipAddress
miner_id = $minerId
mac_address = $macAddress
message = "API3 miner control command sent by Windows Agent."
api3_result = $api3Result
}
throw "physical_control_disabled_stage_a"
}
@@ -1368,8 +989,8 @@ function Process-PendingCommands {
Write-AgentLog "Command completed: $commandId"
} elseif ($commandType -eq "miner_control") {
$result = Run-MinerControlCommand -Payload $cmd.request_payload
Send-AgentCommandResult -CommandId $commandId -Status "completed" -ResultPayload $result | Out-Null
Write-AgentLog "Miner control command completed: $commandId"
Send-AgentCommandResult -CommandId $commandId -Status "failed" -ResultPayload $result | Out-Null
Write-AgentLog "Miner control rejected by Stage A containment: $commandId"
} elseif ($commandType -eq "enrich_match_discovery") {
$payloadObj = $cmd.request_payload
@@ -30,11 +30,57 @@ function Invoke-WhatsMinerApi3Read {
[int] $TimeoutMs = 2500
)
# Exact operation and parameter allowlist at the shared transport boundary.
# No extra keys (including credentials, command aliases, or parameter overrides)
# reach the wire, even when this function is called outside the poller loop.
$readCommands = @("get.device.info", "get.miner.setting", "get.miner.status",
"get.fan.setting", "get.device.custom_data")
$command = $Request["cmd"]
$approvedCommand = $null
if ($command -is [string]) {
foreach ($candidate in $readCommands) {
if ([string]::Equals($candidate, $command, [StringComparison]::Ordinal)) {
$approvedCommand = $candidate
break
}
}
}
if ($null -eq $approvedCommand) {
throw "unsupported_diagnostic_operation_stage_a"
}
$expectedKeys = @("cmd")
$wireRequest = [ordered]@{ cmd = $approvedCommand }
if ([string]::Equals($approvedCommand, "get.miner.status", [StringComparison]::Ordinal)) {
$expectedKeys += "param"
if ($Request["param"] -isnot [string] -or
-not [string]::Equals($Request["param"], "summary+pools+edevs", [StringComparison]::Ordinal)) {
throw "unsupported_diagnostic_operation_stage_a"
}
$wireRequest["param"] = "summary+pools+edevs"
}
if ($Request.Count -ne $expectedKeys.Count) {
throw "unsupported_diagnostic_operation_stage_a"
}
foreach ($key in $Request.Keys) {
$approvedKey = $false
if ($key -is [string]) {
foreach ($expectedKey in $expectedKeys) {
if ([string]::Equals($expectedKey, $key, [StringComparison]::Ordinal)) {
$approvedKey = $true
break
}
}
}
if (-not $approvedKey) {
throw "unsupported_diagnostic_operation_stage_a"
}
}
$client = $null
$stream = $null
try {
$json = ($Request | ConvertTo-Json -Depth 20 -Compress)
$json = ($wireRequest | ConvertTo-Json -Depth 20 -Compress)
$payload = [System.Text.Encoding]::ASCII.GetBytes($json)
$length = [BitConverter]::GetBytes([Int32]$payload.Length)
+9 -110
View File
@@ -26,29 +26,8 @@ function Get-MinerOpsControlCredentials {
}
function Invoke-MinerOpsControlApi3Command {
param(
[Parameter(Mandatory=$true)][string]$IpAddress,
[Parameter(Mandatory=$true)][string]$Command,
[object]$Param = $null
)
$credentials = Get-MinerOpsControlCredentials
if (-not $credentials.available) { throw "credentials_missing" }
$saltInfo = Get-MinerOpsApi3Salt -IpAddress $IpAddress -Port $credentials.port
$timestamp = [int64][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$token = Get-MinerOpsApi3Token -Command $Command -Password $credentials.password `
-Salt ([string]$saltInfo.salt) -Timestamp $timestamp
$payload = @{
cmd = $Command
ts = $timestamp
token = $token
account = $credentials.account
}
if ($null -ne $Param) { $payload["param"] = $Param }
$response = Invoke-MinerOpsApi3Payload -IpAddress $IpAddress `
-Port $credentials.port -Payload $payload
if ([int]$response.response.code -ne 0) { throw "command_rejected" }
return $response
param([string]$IpAddress, [string]$Command, [object]$Param = $null)
throw "physical_control_disabled_stage_a"
}
function New-MinerOpsControlResult {
@@ -74,90 +53,11 @@ function New-MinerOpsControlResult {
function Run-MinerControlCommand {
param([Parameter(Mandatory=$true)][object]$Payload)
try {
if ($Payload -is [string]) { $Payload = $Payload | ConvertFrom-Json }
$action = ([string]$Payload.action).Trim().ToLower()
$ipAddress = ([string]$Payload.ip_address).Trim()
$requested = ([string]$Payload.requested_value).Trim().ToLower()
if (-not $requested -and $Payload.performance_mode) {
$requested = ([string]$Payload.performance_mode).Trim().ToLower()
}
if ($action -notin @("suspend", "resume", "performance_mode")) {
return New-MinerOpsControlResult -Accepted $false `
-VerificationAttempted $false -VerificationStatus "unavailable" `
-FailureCode "invalid_action" -QueueStatus "failed"
}
if ([string]::IsNullOrWhiteSpace($ipAddress)) {
return New-MinerOpsControlResult -Accepted $false `
-VerificationAttempted $false -VerificationStatus "unavailable" `
-FailureCode "target_missing" -QueueStatus "failed"
}
$credentials = Get-MinerOpsControlCredentials
if (-not $credentials.available) {
return New-MinerOpsControlResult -Accepted $false `
-VerificationAttempted $false -VerificationStatus "unavailable" `
-FailureCode "credentials_missing" -QueueStatus "failed"
}
if ($action -eq "performance_mode") {
if ($requested -notin @("low", "normal", "high")) {
return New-MinerOpsControlResult -Accepted $false `
-VerificationAttempted $false -VerificationStatus "unavailable" `
-FailureCode "invalid_mode" -QueueStatus "failed"
}
Invoke-MinerOpsControlApi3Command -IpAddress $ipAddress `
-Command "set.miner.power_mode" -Param $requested | Out-Null
$observed = ""
$readUnavailable = $false
for ($attempt = 1; $attempt -le 3; $attempt++) {
try {
$read = Invoke-MinerOpsControlApi3Command -IpAddress $ipAddress `
-Command "get.miner.setting"
$candidate = ([string]$read.response.msg.'power-mode').Trim().ToLower()
if ($candidate -in @("low", "normal", "high")) {
$observed = $candidate
if ($observed -eq $requested) {
return New-MinerOpsControlResult -Accepted $true `
-VerificationAttempted $true -VerificationStatus "confirmed" `
-ObservedMode $observed
}
}
} catch {
if ($_.Exception.Message -match "unsupported|unknown_command") {
$readUnavailable = $true
break
}
}
if ($attempt -lt 3) { Start-Sleep -Milliseconds 750 }
}
if ($readUnavailable) {
return New-MinerOpsControlResult -Accepted $true `
-VerificationAttempted $true -VerificationStatus "unavailable"
}
if ($observed) {
return New-MinerOpsControlResult -Accepted $true `
-VerificationAttempted $true `
-VerificationStatus "verification_failed" -ObservedMode $observed
}
return New-MinerOpsControlResult -Accepted $true `
-VerificationAttempted $true -VerificationStatus "unconfirmed"
}
$serviceAction = if ($action -eq "suspend") { "stop" } else { "start" }
Invoke-MinerOpsControlApi3Command -IpAddress $ipAddress `
-Command "set.miner.service" -Param $serviceAction | Out-Null
return New-MinerOpsControlResult -Accepted $true `
-VerificationAttempted $false -VerificationStatus "unconfirmed"
} catch {
$failure = if ($_.Exception.Message -eq "credentials_missing") {
"credentials_missing"
} else { "write_rejected" }
return New-MinerOpsControlResult -Accepted $false `
-VerificationAttempted $false -VerificationStatus "unavailable" `
-FailureCode $failure -QueueStatus "failed"
}
# No current request schema binds authorization to physical identity.
# Do not parse, normalize, reinterpret, or transmit an old queued payload.
return New-MinerOpsControlResult -Accepted $false `
-VerificationAttempted $false -VerificationStatus "unavailable" `
-FailureCode "physical_control_disabled_stage_a" -QueueStatus "failed"
}
function Send-AgentCommandResult {
@@ -185,7 +85,6 @@ function Send-AgentCommandResult {
function Send-AgentCheckin {
$config = Read-AgentConfig
$credentials = Get-MinerOpsControlCredentials
$body = [ordered]@{
agent_id = [string]$config.agent_id
site_id = [string]$config.site_id
@@ -193,8 +92,8 @@ function Send-AgentCheckin {
hostname = $env:COMPUTERNAME
version = Get-MinerOpsAgentVersion
ip_address = ""
control_available = [bool]$credentials.available
control_unavailable_reason = if ($credentials.available) { $null } else { "credentials_missing" }
control_available = $false
control_unavailable_reason = "physical_control_disabled_stage_a"
local_time = (Get-Date).ToString("o")
}
return Invoke-AgentApi -Method "POST" -Path "/api/agent/checkin" -Body $body
+38 -1
View File
@@ -1,7 +1,44 @@
# MinerOps Windows Agent
## Stage A physical-control containment
Physical miner control is disabled in this version, including suspend, resume,
performance-mode changes, reboot, M30 tests, and miner-side password changes.
No environment flag, configured API3 credential, or Agent capability report can
enable it. This is containment, not completion of Blocker #3 identity binding.
The Hub returns HTTP 410 for retired legacy command, scheduling, and unrestricted
diagnostic endpoints (after authorization where applicable). Typed control POSTs
require `miners.control` and return HTTP 409 `physical_control_disabled_stage_a`.
The unrestricted `whatsminer-read-test` endpoint is not a supported read-only tool.
Authenticated queue polling cancels pending/claimed physical and unknown command
types, records an atomic `agent.command.rejected` audit entry, and only delivers
fixed telemetry/discovery command types. Results cannot promote retired entries.
Original payloads, prior results/errors, timestamps, and terminal history remain.
Cancellation of an already claimed command does **not** prove it was never sent;
the audit explicitly records its previous execution outcome as unknown.
The Windows executor rejects all current physical-control schemas without any
device connection and advertises physical control as unavailable. Existing Agents
must be stopped/drained and upgraded before claiming fleet-wide containment:
server cancellation cannot recall payloads cached by older running Agents.
Do not enable control or deploy this work before separate security review.
The ordered, fail-closed procedure is in
[the Stage A old-Agent cutover runbook](../../docs/operations/STAGE_A_OLD_AGENT_CUTOVER.txt).
Native Windows PowerShell 5.1/SYSTEM validation and verified fleet network fencing
remain production gates; server cancellation alone cannot contain an old executor.
Retained legacy/API3 enrichment helpers validate exact read commands and fixed
parameters before opening a socket. Unknown operations and extra keys fail closed.
Schedule cancel/delete require `miners.control` and retire matching live unsupported
commands for the stored Agent with atomic actor-attributed rejection audits.
Deletion retains schedule history; retries do not duplicate audits. Previously
claimed execution remains unknown. Manual cutover retirement does not claim read
work and must sweep offline/queue-only Agent IDs until the global live count is zero.
The Windows Agent runs at a mining site and performs local discovery, polling,
API3 enrichment, pool polling, and approved queued miner-control operations.
API3 enrichment, and pool polling. Physical miner control is disabled in Stage A.
MinerOps does not send API3 credentials through the hub command queue; those
credentials remain in the Agent's local `agent-config.json`.
+103 -72
View File
@@ -1,84 +1,115 @@
$ErrorActionPreference = "Stop"
$script:config = [pscustomobject]@{ api3_account = "operator"; api3_password = "secret"; api3_port = 4433 }
$script:calls = @()
$script:outcomes = @()
$script:outcomeIndex = 0
function Read-AgentConfig { return $script:config }
. (Join-Path (Split-Path $PSScriptRoot -Parent) "MinerOpsControl.ps1")
function Invoke-MinerOpsControlApi3Command {
param([string]$IpAddress, [string]$Command, [object]$Param = $null)
$script:calls += [pscustomobject]@{ command = $Command; param = $Param }
$outcome = $script:outcomes[$script:outcomeIndex]
$script:outcomeIndex += 1
if ($outcome -is [System.Exception]) { throw $outcome }
return $outcome
}
$script:assertions = 0
$script:physicalCalls = 0
$script:readCalls = 0
$script:apiCalls = @()
$script:commands = @()
function Assert-Equal($Actual, $Expected, [string]$Message) {
$script:assertions++
if ($Actual -ne $Expected) { throw "$Message. Expected '$Expected', got '$Actual'." }
}
function Reset-Harness([object[]]$Outcomes) {
$script:calls = @()
$script:outcomes = @($Outcomes)
$script:outcomeIndex = 0
}
function Response([string]$Mode = "") {
return @{ response = @{ code = 0; msg = @{ "power-mode" = $Mode } } }
}
$payload = @{ action = "performance_mode"; requested_value = "normal"; ip_address = "192.0.2.1" }
$script:config = [pscustomobject]@{ api3_account = ""; api3_password = ""; api3_port = 4433 }
Reset-Harness @()
$result = Run-MinerControlCommand -Payload $payload
Assert-Equal $result.failure_code "credentials_missing" "Missing credentials code"
Assert-Equal $script:calls.Count 0 "Missing credentials must not contact API3"
$script:config = [pscustomobject]@{ api3_account = "operator"; api3_password = "secret"; api3_port = 4433 }
Reset-Harness @((Response), (Response "normal"))
$result = Run-MinerControlCommand -Payload $payload
Assert-Equal $result.verification_status "confirmed" "Performance confirmation"
Assert-Equal @($script:calls | Where-Object command -Like "set.*").Count 1 "Exactly one write"
Reset-Harness @((Response), (Response "low"), (Response "low"), (Response "low"))
$result = Run-MinerControlCommand -Payload $payload
Assert-Equal $result.verification_status "verification_failed" "Contradictory mode"
Assert-Equal $script:calls.Count 4 "One write and three bounded reads"
Reset-Harness @((Response), (Response), (Response), (Response))
$result = Run-MinerControlCommand -Payload $payload
Assert-Equal $result.verification_status "unconfirmed" "Ambiguous reads"
Reset-Harness @((Response), [System.Exception]::new("unknown_command"))
$result = Run-MinerControlCommand -Payload $payload
Assert-Equal $result.verification_status "unavailable" "Unavailable read path"
foreach ($action in @("suspend", "resume")) {
Reset-Harness @((Response))
$result = Run-MinerControlCommand -Payload @{
action = $action; requested_value = $null; ip_address = "192.0.2.1"
function Assert-Throws($Block, [string]$Message) {
$caught = $false
try { & $Block | Out-Null } catch {
$caught = $true
Assert-Equal $_.Exception.Message "physical_control_disabled_stage_a" $Message
}
Assert-Equal $result.command_accepted $true "$action accepted"
Assert-Equal $result.verification_status "unconfirmed" "$action remains unconfirmed"
Assert-Equal $result.observed_service_state $null "$action has no invented state"
Assert-Equal @($script:calls | Where-Object command -Like "set.*").Count 1 "$action one write"
Assert-Equal $caught $true "$Message must reject"
}
Reset-Harness @([System.Exception]::new("vendor response included secret"))
$result = Run-MinerControlCommand -Payload @{
action = "suspend"; requested_value = $null; ip_address = "192.0.2.1"
# Define functions from the real service AST, without executing its service loop,
# config loading, startup, or any scheduled tasks.
$root = Split-Path $PSScriptRoot -Parent
$tokens = $null; $parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
(Join-Path $root "MinerOpsAgentService.ps1"), [ref]$tokens, [ref]$parseErrors)
Assert-Equal $parseErrors.Count 0 "Service parser"
foreach ($function in $ast.FindAll({
param($node)
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
$node.Parent -is [System.Management.Automation.Language.NamedBlockAst]
}, $true)) {
Invoke-Expression $function.Extent.Text
}
Assert-Equal $result.failure_code "write_rejected" "Write rejection is sanitized"
$json = $result | ConvertTo-Json -Compress
if ($json -match "vendor|secret|192\\.0\\.2\\.1") { throw "Typed result leaked raw data" }
Reset-Harness @()
$result = Run-MinerControlCommand -Payload @{
action = "performance_mode"; requested_value = "turbo"; ip_address = "192.0.2.1"
$script:productionApi3Payload = (Get-Command Invoke-MinerOpsApi3Payload).ScriptBlock
$script:productionPlainCommand = (Get-Command Invoke-MinerPlainCommand).ScriptBlock
function Invoke-MinerOpsApi3Payload { $script:physicalCalls++; throw "Unexpected API3 transport" }
function Get-MinerOpsApi3Salt { $script:physicalCalls++; throw "Unexpected salt probe" }
function Invoke-MinerPlainCommand { $script:physicalCalls++; throw "Unexpected legacy transport" }
function Read-AgentConfig { return [pscustomobject]@{
agent_id = "simulated-agent"; site_id = ""; site_name = "Simulation"
api3_account = "operator"; api3_password = "mock-only"; api3_port = 4433
} }
function Write-AgentLog { param([string]$Message) }
function Invoke-AgentApi {
param([string]$Method, [string]$Path, [object]$Body)
$script:apiCalls += @{ method = $Method; path = $Path; body = $Body }
return @{ ok = $true }
}
Assert-Equal $result.failure_code "invalid_mode" "Invalid mode code"
Assert-Equal $script:calls.Count 0 "Invalid mode must not contact API3"
function Get-PendingAgentCommands { return $script:commands }
function Run-KnownMinerPoll { $script:readCalls++; return @{ simulated = $true } }
function Run-NewMinerDiscovery { $script:readCalls++; return @{ simulated = $true } }
Write-Host "MinerOps typed Agent control tests passed."
# The pre-module legacy implementation must also be inert, including helpers
# which used to support direct generic API3 or M30/4028 writes.
Assert-Throws { Run-MinerControlCommand -Payload @{ action = "reboot"; dry_run = $false } } "Legacy executor"
Assert-Throws { Invoke-MinerOpsApi3Service -IpAddress "192.0.2.1" -ServiceAction "stop" } "Legacy service helper"
Assert-Throws { & $script:productionApi3Payload -IpAddress "192.0.2.1" -Payload @{ cmd = "set.miner.service"; param = "stop" } } "Generic API3 payload helper"
Assert-Throws { & $script:productionPlainCommand -IpAddress "192.0.2.1" -Command "reboot" } "Generic legacy command helper"
Assert-Throws { Invoke-MinerOpsApi3AuthenticatedCommand -IpAddress "192.0.2.1" -Command "set.miner.service" -Param "stop" } "Legacy API3"
Assert-Throws { Invoke-MinerOpsLegacy4028Command -IpAddress "192.0.2.1" -CommandJson '{"command":"reboot"}' } "Legacy 4028"
Assert-Throws { Invoke-MinerOpsM30Legacy4028Test -Payload @{ legacy_4028_command = "reboot" } } "M30 helper"
. (Join-Path $root "MinerOpsControl.ps1")
Assert-Throws { Invoke-MinerOpsControlApi3Command -IpAddress "192.0.2.1" -Command "set.miner.service" -Param "stop" } "Typed transport"
Assert-Throws { Invoke-MinerOpsControlApi3Command -IpAddress "192.0.2.1" -Command "get.read_then_write" } "Disguised diagnostic"
$payloads = @(
@{ action = "legacy_4028"; legacy_4028_command = "reboot"; dry_run = $false },
@{ action = "reboot"; ip_address = "192.0.2.1" },
@{ schema_version = 1; action = "suspend"; ip_address = "192.0.2.1"; miner_id = "spoofed" },
@{ schema_version = 1; action = "resume"; ip_address = "192.0.2.1" },
@{ schema_version = 1; action = "performance_mode"; requested_value = "high"; ip_address = "192.0.2.1" },
@{ schema_version = 999; action = "suspend" },
@{ schema_version = "1"; action = "performance_mode"; performance_mode = "high" },
@{ command = "summary"; params = @{ cmd = "set.miner.service"; param = "stop" } },
@{ action = "read_reboot"; dry_run = $true },
@{},
'{"schema_version":0,"action":"reboot"}',
"{invalid-json"
)
foreach ($payload in $payloads) {
$result = Run-MinerControlCommand -Payload $payload
Assert-Equal $result.command_accepted $false "Rejected payload acceptance"
Assert-Equal $result.failure_code "physical_control_disabled_stage_a" "Rejected payload reason"
Assert-Equal $result._queue_status "failed" "Rejected payload terminal result"
Assert-Equal $result.verification_attempted $false "No physical verification/probe"
Assert-Equal $result.observed_service_state $null "No invented device state"
Assert-Equal $script:physicalCalls 0 "No physical calls"
$script:commands = @(@{ command_id = "legacy-delivery"; command_type = "miner_control"; request_payload = $payload })
$script:apiCalls = @()
Assert-Equal (Process-PendingCommands) 1 "Delivered legacy command handled"
Assert-Equal $script:apiCalls.Count 1 "One failed result submitted"
Assert-Equal $script:apiCalls[0].body.status "failed" "Legacy delivery failed"
Assert-Equal $script:apiCalls[0].body.result_payload.command_accepted $false "Legacy delivery not accepted"
Assert-Equal $script:physicalCalls 0 "No writes after delivery"
}
foreach ($kind in @("raw_api3", "miner_control_v2", "read_reboot", "")) {
$script:commands = @(@{ command_id = "unsupported"; command_type = $kind; request_payload = @{ action = "reboot" } })
$script:apiCalls = @()
Assert-Equal (Process-PendingCommands) 1 "Unknown command handled"
Assert-Equal $script:apiCalls[0].body.status "failed" "Unknown command fails closed"
}
Assert-Equal $script:readCalls 0 "No physical request reinterpreted as telemetry"
$script:commands = @(@{ command_id = "read-only"; command_type = "poll_known_miners_now"; request_payload = @{} })
$script:apiCalls = @()
Assert-Equal (Process-PendingCommands) 1 "Fixed read-only command available"
Assert-Equal $script:readCalls 1 "Only simulated read performed"
Assert-Equal $script:physicalCalls 0 "No physical calls in entire suite"
$script:apiCalls = @()
Send-AgentCheckin | Out-Null
Assert-Equal $script:apiCalls[0].body.control_available $false "Cannot advertise physical capability"
Assert-Equal $script:apiCalls[0].body.control_unavailable_reason "physical_control_disabled_stage_a" "Containment advertised"
Write-Host "Stage A Windows containment passed: $($script:assertions) assertions; zero physical transport calls."
@@ -0,0 +1,199 @@
$ErrorActionPreference = "Stop"
$script:assertions = 0
$script:connections = 0
$TimeoutMs = 1000
function Assert-True([bool]$Condition, [string]$Message) {
$script:assertions++
if (-not $Condition) { throw $Message }
}
function Assert-Rejected([scriptblock]$Call) {
$before = $script:connections
$caught = $false
try { & $Call | Out-Null } catch {
$caught = $true
Assert-True ($_.Exception.Message -eq "unsupported_diagnostic_operation_stage_a") "Must reject before transport"
}
Assert-True $caught "Unsupported diagnostics must throw"
Assert-True ($script:connections -eq $before) "Rejected request must create zero sockets and write zero bytes"
}
function Import-Definitions([string]$Path) {
$tokens = $null; $errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors)
Assert-True ($errors.Count -eq 0) "Production script must parse"
foreach ($definition in $ast.FindAll({
param($node)
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
$node.Parent -is [System.Management.Automation.Language.NamedBlockAst]
}, $true)) {
. ([scriptblock]::Create("function global:" + $definition.Name + " " + $definition.Body.Extent.Text))
}
}
# Load only real function definitions: no config, Hub request, or scheduled loop.
$root = Split-Path $PSScriptRoot -Parent
Import-Definitions (Join-Path $root "EnrichOneMiner.ps1")
Import-Definitions (Join-Path $root "MinerOpsApi3EnrichmentPoller.ps1")
Import-Definitions (Join-Path $root "MinerOpsPoolPoller.ps1")
function New-Object {
param([Parameter(Position=0)][string]$TypeName,
[Parameter(Position=1)][object[]]$ArgumentList)
if ($TypeName -eq "System.Net.Sockets.TcpClient") { $script:connections++ }
if ($null -ne $ArgumentList) {
return Microsoft.PowerShell.Utility\New-Object -TypeName $TypeName -ArgumentList $ArgumentList
}
return Microsoft.PowerShell.Utility\New-Object -TypeName $TypeName
}
# Auditor reproductions plus write families, alias/parameter overrides, injection,
# malformed structures and operations that merely contain a read-looking substring.
foreach ($command in @("reboot", "set.miner.service", "power_off", "switchpool", "ascset",
"summary|reboot", "summary`nreboot", 'summary","cmd":"reboot', "read_then_reboot",
"SUMMARY", "", "get.device.info", "get_miner_info|all")) {
Assert-Rejected { Invoke-MinerCommandRaw -IpAddress "127.0.0.1" -Command $command }
}
Assert-Rejected { Invoke-MinerCommandRaw -IpAddress "127.0.0.1" -Command "summary" -Port 4433 }
$badRequests = @(
@{ cmd = "set.miner.service"; param = "stop" },
@{ cmd = "reboot" }, @{ cmd = "set.miner.power_mode"; param = "high" },
@{ cmd = "set.device.password"; param = "changed" }, @{ cmd = "get.read_then_write" },
@{ cmd = "get.device.info"; command = "reboot" },
@{ cmd = "get.device.info"; param = "stop" },
@{ cmd = "get.device.info"; account = "operator"; token = "anything" },
@{ cmd = "get.miner.status" }, @{ cmd = "get.miner.status"; param = "all" },
@{ cmd = "get.miner.status"; param = @("summary+pools+edevs") },
@{ cmd = "get.miner.status"; param = "summary+pools+edevs"; command = "reboot" },
@{ Cmd = "get.device.info" }, @{ cmd = "GET.DEVICE.INFO" },
@{ cmd = "get.device.info`nreboot" }, @{ cmd = @("get.device.info", "reboot") },
@{ cmd = $null }, @{}
)
foreach ($request in $badRequests) {
Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request $request }
Assert-Rejected { Invoke-Api3OkCommand -Ip "127.0.0.1" -Request $request }
}
# Exact re-audit suffixes: U+0000, U+00AD and U+200B; also prefix/embedded
# invisible characters, controls and whitespace. Never trim or normalize inputs.
$originalCulture = [Threading.Thread]::CurrentThread.CurrentCulture
try {
foreach ($cultureName in @("en-US", "tr-TR", "")) {
[Threading.Thread]::CurrentThread.CurrentCulture = [Globalization.CultureInfo]::GetCultureInfo($cultureName)
$variants = @()
foreach ($code in @(0, 0x00ad, 0x200b, 0x200c, 0x200d, 0x2060, 0xfeff,
0x001f, 13, 10, 9, 32, 0x00a0)) {
$character = [string][char]$code
foreach ($position in @("prefix", "embedded", "suffix")) {
$variants += @{ Character = $character; Position = $position }
}
}
foreach ($variant in $variants) {
$legacy = "summary"; $api = "get.device.info"; $key = "cmd"; $param = "summary+pools+edevs"
if ($variant.Position -eq "prefix") {
$legacy = $variant.Character + $legacy; $api = $variant.Character + $api
$key = $variant.Character + $key; $param = $variant.Character + $param
} elseif ($variant.Position -eq "embedded") {
$legacy = "sum" + $variant.Character + "mary"; $api = "get.device." + $variant.Character + "info"
$key = "c" + $variant.Character + "md"; $param = "summary+" + $variant.Character + "pools+edevs"
} else {
$legacy += $variant.Character; $api += $variant.Character
$key += $variant.Character; $param += $variant.Character
}
Assert-Rejected { Invoke-MinerCommandRaw -IpAddress "127.0.0.1" -Command $legacy }
Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request @{ cmd = $api } }
Assert-Rejected { Invoke-Api3OkCommand -Ip "127.0.0.1" -Request @{ cmd = $api } }
$request = @{}; $request[$key] = "get.device.info"
Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request $request }
Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request @{ cmd = "get.miner.status"; param = $param } }
$request = @{ cmd = "get.miner.status" }; $request[$key.Replace("cmd", "param")] = "summary+pools+edevs"
Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request $request }
}
foreach ($legacy in @(("summar" + [char]0x0443), ([char]0xff53 + "ummary"),
("summ" + [char]0x0061 + [char]0x0301 + "ry"),
("summ" + [char]0x00e1 + "ry"), "Summary", "summary/", "summary+")) {
Assert-Rejected { Invoke-MinerCommandRaw -IpAddress "127.0.0.1" -Command $legacy }
}
foreach ($api in @(("get.dev" + [char]0x0456 + "ce.info"), ("get" + [char]0xff0e + "device.info"),
([char]0xff47 + "et.device.info"), "Get.Device.Info", "get.device.info/", "get.device.info+")) {
Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request @{cmd=$api} }
}
Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request @{cmd="get.miner.status"; Param="summary+pools+edevs"} }
Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request @{cmd="get.miner.status"; param="Summary+pools+edevs"} }
}
} finally { [Threading.Thread]::CurrentThread.CurrentCulture = $originalCulture }
Assert-True ($script:connections -eq 0) "All negative cases produced zero transport connections/writes"
# Real TCP compatibility tests: bind loopback only, one request per simulator.
# This suite must be run with physical networks disconnected (Docker --network none).
Add-Type -TypeDefinition @'
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
public class MinerOpsDiagnosticSimulator : IDisposable {
TcpListener listener;
Thread thread;
public string Payload;
public string Error;
public MinerOpsDiagnosticSimulator(int port, bool framed) {
listener = new TcpListener(IPAddress.Loopback, port); listener.Start();
thread = new Thread(() => {
try {
using (var client = listener.AcceptTcpClient()) {
client.ReceiveTimeout = 2000;
var stream = client.GetStream(); var buffer = new byte[65536]; int count;
if (framed) {
var length = new byte[4]; ReadExact(stream, length, 4);
count = BitConverter.ToInt32(length, 0);
if(count < 1 || count > buffer.Length) throw new Exception("Bad request length");
ReadExact(stream, buffer, count);
} else { count = stream.Read(buffer, 0, buffer.Length); }
Payload = Encoding.ASCII.GetString(buffer, 0, count);
var response = Encoding.ASCII.GetBytes("{\"code\":0,\"POOLS\":[],\"msg\":{}}");
if(framed) { var length = BitConverter.GetBytes(response.Length); stream.Write(length,0,4); }
stream.Write(response, 0, response.Length);
}
} catch(Exception e) { Error = e.Message; }
finally { listener.Stop(); }
}); thread.IsBackground = true; thread.Start();
}
static void ReadExact(NetworkStream stream, byte[] bytes, int count) {
int offset=0;
while(offset<count) { int n=stream.Read(bytes,offset,count-offset); if(n==0) throw new Exception("EOF"); offset+=n; }
}
public void Dispose() { listener.Stop(); thread.Join(3000); }
}
'@
foreach ($command in @("summary", "devs", "edevs", "stats", "pools", "devdetails", "version", "get_version", "get_miner_info")) {
$miner = [MinerOpsDiagnosticSimulator]::new(4028, $false)
try {
$null = Invoke-MinerCommandRaw -IpAddress "127.0.0.1" -Command $command -TimeoutMs 100
$observed = $miner.Payload | ConvertFrom-Json
Assert-True ([string]::Equals($observed.cmd, $command, [StringComparison]::Ordinal)) "Legacy query must remain compatible"
Assert-True ($observed.PSObject.Properties.Count -eq 1) "Legacy request contains only selected read"
Assert-True (-not $miner.Error) "Simulator received a valid legacy request"
} finally { $miner.Dispose() }
}
foreach ($command in @("get.device.info", "get.miner.setting", "get.miner.status", "get.fan.setting", "get.device.custom_data")) {
$request = @{ cmd = $command }
if ($command -ceq "get.miner.status") { $request.param = "summary+pools+edevs" }
$miner = [MinerOpsDiagnosticSimulator]::new(4433, $true)
try {
$result = Invoke-Api3OkCommand -Ip "127.0.0.1" -Request $request
$observed = $miner.Payload | ConvertFrom-Json
Assert-True ($null -ne $result) "Existing API3 reader wrapper remains compatible"
Assert-True ([string]::Equals($observed.cmd, $command, [StringComparison]::Ordinal)) "Exact API3 query transmitted"
Assert-True (@($observed.PSObject.Properties).Count -eq $request.Count) "No additional API3 request keys"
if ($command -ceq "get.miner.status") {
Assert-True ([string]::Equals($observed.param, "summary+pools+edevs", [StringComparison]::Ordinal)) "Only exact read parameter transmitted"
}
Assert-True (-not $miner.Error) "Simulator received a valid framed read"
} finally { $miner.Dispose() }
}
$miner = [MinerOpsDiagnosticSimulator]::new(4028, $false)
try {
$null = Invoke-MinerOps4028PoolsSimple -Ip "127.0.0.1"
Assert-True (($miner.Payload | ConvertFrom-Json).command -ceq "pools") "Pool poller sends a constant read"
} finally { $miner.Dispose() }
Write-Host "Diagnostic containment passed: $($script:assertions) assertions; all rejected operations made zero connections/writes."
@@ -0,0 +1,46 @@
param(
[Parameter(Mandatory=$true)][string]$BaselineRoot,
[Parameter(Mandatory=$true)][string]$CachedPayloadPath,
[Parameter(Mandatory=$true)][string]$MinerIp,
[switch]$ExpectFenced
)
$ErrorActionPreference = "Stop"
if ($env:MINEROPS_STAGE_A_SIMULATION -ne "1") {
throw "Requires an explicitly isolated Stage A simulation; never use physical targets."
}
# Supply a git-archived 2d89e80 Windows source directory, NOT a running installation.
# Only AST function definitions execute; no startup, config loading, or task loops.
$tokens = $null; $errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
(Join-Path $BaselineRoot "MinerOpsAgentService.ps1"), [ref]$tokens, [ref]$errors)
if ($errors.Count) { throw "Baseline parser error" }
foreach ($definition in $ast.FindAll({
param($node)
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
$node.Parent -is [System.Management.Automation.Language.NamedBlockAst]
}, $true)) {
Invoke-Expression $definition.Extent.Text
}
. (Join-Path $BaselineRoot "MinerOpsControl.ps1")
function Read-AgentConfig {
return [pscustomobject]@{
api3_account = "simulation"; api3_password = "simulation-only"; api3_port = 4433
}
}
function Get-MinerOpsApi3Salt { return @{ salt = "simulation-only"; salt_response = @{} } }
$payload = Get-Content -LiteralPath $CachedPayloadPath -Raw | ConvertFrom-Json
if ($payload.action -ne "suspend") { throw "This simulation expects the audited cached suspend command" }
# Remap ONLY the target to the address of the dedicated disposable miner container.
$payload.ip_address = $MinerIp
$result = Run-MinerControlCommand -Payload $payload
if ($ExpectFenced) {
if ($result.command_accepted -ne $false -or $result.failure_code -ne "write_rejected") {
throw "Old executor unexpectedly reached/accepted a simulated miner through the fence"
}
Write-Host "Fenced old cached command failed; independently verify simulator write count stayed zero."
} else {
if ($result.command_accepted -ne $true) { throw "Unfenced control simulation did not reproduce old execution" }
Write-Host "Unfenced old cached command reproduced; simulated miner received service stop."
}