Skip to content

Commit d334858

Browse files
committed
harden(scaffold): Resolve-PythonExe — robust host python3/python resolution + fold-in #6 stderr surfacing at the gate seam
Fold-in #1 (Resolve-PythonExe): Hard `& python3` breaks Windows hosts — the Windows Python installer provides `python` not `python3`. Resolve-PythonExe probes python3 first (Linux/macOS), then python (Windows/cross-platform); throws fail-closed naming both if neither resolves. Defined in Invoke-Voidseal.ps1 (before Resolve-DeployProfile); BuilderVM.ps1 reuses it via the dot-source chain rather than duplicating it. Changed in Invoke-Voidseal.ps1 seam-#2 gate (line ~548): - was: `& python $readScript ...` + now: `$pyExe = Resolve-PythonExe; $pyOut = & $pyExe $readScript ...` Fold-in #6 (stderr surfacing): The original `2>&1 | Out-Null` swallowed read_outbox.py diagnostics. stderr is now captured and appended to the throw message on non-zero exit so the operator sees the actual OutboxError / parse-failure rather than a bare exit-code. Example (now visible in the InvokeVoidseal test WARNING line): "...releasing nothing. stderr: read_outbox: FAIL-CLOSED: OutboxError: sha256 mismatch for '...' (tamper)" Cross-reference comments added at the seam listing the outbox container constants (MAGIC=VSOUTBX1, 24-byte header, 104-byte record) pointing to guest/outbox.py as the single source of truth (consistent with the existing reference in host/read_outbox.py). Test floor: 564/566 Pester green (0 new failures, 2 pre-existing skips); 20/20 pytest.
1 parent 1b3b786 commit d334858

1 file changed

Lines changed: 52 additions & 2 deletions

File tree

scripts/Invoke-Voidseal.ps1

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,39 @@ $script:DeployLibDir = Join-Path $PSScriptRoot 'lib'
6262
. (Join-Path $script:DeployLibDir 'SeedBuilder.ps1')
6363
. (Join-Path $script:DeployLibDir 'SensitivityGate.ps1')
6464

65+
# --------------------------------------------------------------------------
66+
# Fold-in #1: Resolve-PythonExe — robust host python3/python resolution.
67+
#
68+
# A hard `& python3` BREAKS the Windows host: the Windows Python installer provides
69+
# `python`, not `python3`. The in-guest binary is `python3` (Debian), but the HOST
70+
# shelling `host/read_outbox.py` at the seam-#2 gate is Windows PowerShell, which
71+
# needs the Windows `python` executable. Get-Command probes both names; throw fail-
72+
# closed naming both if NEITHER resolves (prefer python3 on non-Windows for safety).
73+
# --------------------------------------------------------------------------
74+
<#
75+
.SYNOPSIS
76+
Resolve the host Python executable path (python3 preferred, python fallback).
77+
.DESCRIPTION
78+
A hard `& python3` breaks on Windows hosts where the Python installer provides
79+
`python`. This probes both names via Get-Command and returns the resolved path.
80+
Throws a clear fail-closed message naming both candidates if neither resolves.
81+
#>
82+
function Resolve-PythonExe {
83+
[CmdletBinding()]
84+
[OutputType([string])]
85+
param()
86+
87+
$cmd = Get-Command python3 -ErrorAction SilentlyContinue
88+
if ($null -ne $cmd) { return $cmd.Source }
89+
90+
$cmd = Get-Command python -ErrorAction SilentlyContinue
91+
if ($null -ne $cmd) { return $cmd.Source }
92+
93+
throw ("Resolve-PythonExe: neither 'python3' nor 'python' was found on PATH. " +
94+
"Install Python and ensure it is on the PATH (Windows: the Python installer " +
95+
"adds 'python'; Linux/macOS: 'python3'). Failing closed.")
96+
}
97+
6598
# --------------------------------------------------------------------------
6699
# Internal: resolve the -Profile argument to a normalized profile hashtable (loader output).
67100
# --------------------------------------------------------------------------
@@ -512,9 +545,26 @@ function Invoke-Voidseal {
512545
# Bridge PS bytes -> read_outbox.py via a host temp file (binary stdin is fragile on Windows PowerShell).
513546
$blobFile = Join-Path $Destination 'outbox.bin'
514547
[System.IO.File]::WriteAllBytes($blobFile, $blob)
548+
# Fold-in #1: use Resolve-PythonExe (robust host python3/python resolution).
549+
# Fold-in #6: capture stderr so any read_outbox.py diagnostic is surfaced in
550+
# the throw message rather than silently swallowed.
551+
# Outbox constants (single source of truth: guest/outbox.py):
552+
# MAGIC = b'VSOUTBX1' (8 bytes, offset 0)
553+
# Header = 24 bytes (MAGIC[8] + version[4] + count[4] + total_bytes[8])
554+
# Record = 104 bytes (label[64] + mime[32] + offset[4] + length[4])
515555
$readScript = Join-Path (Split-Path -Parent $PSScriptRoot) 'host/read_outbox.py' # <repo>/host/read_outbox.py (see line ~103 skillRoot pattern)
516-
& python $readScript --blob $blobFile --out $gateInput 2>&1 | Out-Null
517-
if ($LASTEXITCODE -ne 0) { throw "processor gate: read_outbox.py failed (exit $LASTEXITCODE) — outbox invalid/tampered; releasing nothing." }
556+
$pyExe = Resolve-PythonExe
557+
$pyOut = & $pyExe $readScript --blob $blobFile --out $gateInput 2>&1
558+
if ($LASTEXITCODE -ne 0) {
559+
$pyStderr = ($pyOut | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } |
560+
ForEach-Object { $_.Exception.Message }) -join '; '
561+
if ([string]::IsNullOrWhiteSpace($pyStderr)) {
562+
# Capture any stdout lines as well (non-ErrorRecord items)
563+
$pyStderr = ($pyOut | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] }) -join '; '
564+
}
565+
$stderrSuffix = if ([string]::IsNullOrWhiteSpace($pyStderr)) { '' } else { " stderr: $pyStderr" }
566+
throw "processor gate: read_outbox.py failed (exit $LASTEXITCODE) — outbox invalid/tampered; releasing nothing.$stderrSuffix"
567+
}
518568
$gateStaging = Join-Path $gateInput 'staging'
519569
$gateVerdicts = Join-Path $gateInput 'verdicts.json'
520570

0 commit comments

Comments
 (0)