mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 00:02:19 +00:00
ci: qualify stable Bun provider through native SSH
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
name: Diagnostic stable Bun ARM SSH provider qualification
|
||||
on:
|
||||
push:
|
||||
branches: [OrcaWin/np-windows-ssh-provider-diagnostic]
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
concurrency:
|
||||
group: arm-ssh-provider-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
windows_watcher:
|
||||
if: github.repository == 'stablyai/orca' && github.ref == 'refs/heads/OrcaWin/np-windows-ssh-provider-diagnostic'
|
||||
uses: ./.github/workflows/windows-watcher-artifacts.yml
|
||||
with:
|
||||
ref: 2084c58ba5410106ce61153a9fb16cdb4b6e5301
|
||||
qualify:
|
||||
needs: windows_watcher
|
||||
runs-on: windows-11-arm
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
ORCA_ISOLATED_SSH_CI: '1'
|
||||
QUALIFICATION_SOURCE_SHA: 2084c58ba5410106ce61153a9fb16cdb4b6e5301
|
||||
PRODUCER_RUN: '36503596770'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 2084c58ba5410106ce61153a9fb16cdb4b6e5301
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
path: .build/qualification-tools
|
||||
sparse-checkout: config/ci/windows-ssh-provider
|
||||
persist-credentials: false
|
||||
- name: Require matching product and watcher source
|
||||
shell: pwsh
|
||||
env:
|
||||
WATCHER_SOURCE_SHA: ${{ needs.windows_watcher.outputs.source_sha }}
|
||||
run: |
|
||||
if((git rev-parse HEAD).Trim() -ne $env:QUALIFICATION_SOURCE_SHA -or $env:WATCHER_SOURCE_SHA -ne $env:QUALIFICATION_SOURCE_SHA){throw 'Product/watcher source mismatch'}
|
||||
New-Item -ItemType Directory -Force .build/ssh-provider-receipts | Out-Null
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
run-id: '36503596770'
|
||||
name: full-offline-patched-bun
|
||||
path: .build/producer
|
||||
- name: Retain real producer run identity
|
||||
shell: pwsh
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh api "repos/stablyai/orca/actions/runs/$env:PRODUCER_RUN" | Out-File -Encoding utf8NoBOM .build/producer/producer-run.json
|
||||
if($LASTEXITCODE -ne 0){throw 'Producer identity lookup failed'}
|
||||
- uses: ./.github/actions/load-windows-watcher-artifacts
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
native-runtime: node
|
||||
- name: Admit exact stable candidate before provisioning
|
||||
shell: pwsh
|
||||
run: |
|
||||
$tools=Join-Path $pwd '.build/qualification-tools/config/ci/windows-ssh-provider'
|
||||
node (Join-Path $tools 'verify-bun-output.mjs') candidate .build/producer . arm64 .build/ssh-provider-receipts/candidate.json $env:PRODUCER_RUN
|
||||
if($LASTEXITCODE -ne 0){throw 'Stable producer/candidate admission failed'}
|
||||
$receipt=(Resolve-Path .build/ssh-provider-receipts/candidate.json).Path
|
||||
$hash=(Get-FileHash -Algorithm SHA256 -LiteralPath $receipt).Hash.ToLowerInvariant()
|
||||
"CANDIDATE_RECEIPT=$receipt" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"CANDIDATE_RECEIPT_SHA256=$hash" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
@{candidateReceiptSha256=$hash;producerRun=$env:PRODUCER_RUN;product=$env:QUALIFICATION_SOURCE_SHA;candidateOverride=$true;expectedSourcePin=$false;scope='diagnostic in-memory ARM executable pin and private cache only'} | ConvertTo-Json | Set-Content .build/ssh-provider-receipts/admission.json
|
||||
- name: Build production relay artifacts and native process table
|
||||
shell: pwsh
|
||||
run: |
|
||||
node config/scripts/build-cli-runtime.mjs --platform win32 --arch arm64
|
||||
if($LASTEXITCODE -ne 0){throw 'CLI runtime build failed'}
|
||||
node config/scripts/build-windows-process-tree-relay-addon.mjs --arch=arm64
|
||||
if($LASTEXITCODE -ne 0){throw 'Native process-table build failed'}
|
||||
$env:ORCA_REQUIRE_RELAY_NATIVE_ADDONS='arm64'
|
||||
node config/scripts/build-relay.mjs
|
||||
if($LASTEXITCODE -ne 0){throw 'Relay build failed'}
|
||||
- name: Run watcher fault harness with production-pinned CLI Bun
|
||||
shell: pwsh
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
Write-Output 'Scope: production-pinned bundled CLI Bun; actual SSH provider qualification separately uses the admitted stable candidate.' | Tee-Object .build/ssh-provider-receipts/watcher-fault-arm64.log
|
||||
node config/scripts/relay-watcher-fault-harness.mjs 2>&1 | Tee-Object -Append .build/ssh-provider-receipts/watcher-fault-arm64.log
|
||||
if($LASTEXITCODE -ne 0){throw 'Production-pinned CLI Bun watcher fault harness failed'}
|
||||
- name: Parse and typecheck all fixture code before provisioning
|
||||
shell: pwsh
|
||||
run: |
|
||||
$tools=Join-Path $pwd '.build/qualification-tools/config/ci/windows-ssh-provider'
|
||||
foreach($file in @((Join-Path $tools 'preview-ssh/prove-preview-openssh.ps1'),(Join-Path $tools 'preview-ssh/test-preview-diagnostics.ps1'),(Join-Path $tools 'invoke-provider-route.ps1'))){
|
||||
$errors=$null;$tokens=$null
|
||||
[Management.Automation.Language.Parser]::ParseFile($file,[ref]$tokens,[ref]$errors)|Out-Null
|
||||
if($errors.Count){throw "PowerShell parse failed: $file"}
|
||||
}
|
||||
& (Join-Path $tools 'preview-ssh/test-preview-diagnostics.ps1')
|
||||
$copied=[Collections.Generic.List[string]]::new()
|
||||
try {
|
||||
foreach($name in @('windows-arm-provider-route.test.ts','windows-provider-candidate.ts','windows-provider-repaint.ts','windows-provider-loaded-images.ts')){
|
||||
$destination=Join-Path $pwd "src/main/ssh/$name"
|
||||
if(Test-Path -LiteralPath $destination){throw 'Fixture destination already exists'}
|
||||
Copy-Item -LiteralPath (Join-Path $tools $name) -Destination $destination
|
||||
$copied.Add($destination)
|
||||
}
|
||||
node node_modules/typescript/bin/tsc --noEmit -p config/tsconfig.node.json 2>&1 | Tee-Object .build/ssh-provider-receipts/typecheck.log
|
||||
if($LASTEXITCODE -ne 0){throw 'Fixture typecheck failed'}
|
||||
} finally {
|
||||
foreach($destination in $copied){Remove-Item -LiteralPath $destination -Force}
|
||||
}
|
||||
- name: Qualify standard-user SSH deployment, loaded provider and exact resize replay
|
||||
shell: pwsh
|
||||
timeout-minutes: 18
|
||||
run: |
|
||||
$tools=Join-Path $pwd '.build/qualification-tools/config/ci/windows-ssh-provider'
|
||||
$sourceRoot=$pwd.Path
|
||||
$archive=Join-Path $env:RUNNER_TEMP 'preview-OpenSSH-ARM64.zip'
|
||||
& "$env:WINDIR\System32\curl.exe" --fail --location --connect-timeout 15 --max-time 90 --output $archive 'https://github.com/PowerShell/Win32-OpenSSH/releases/download/10.0.0.0p2-Preview/OpenSSH-ARM64.zip'
|
||||
if($LASTEXITCODE -ne 0){throw 'SSH archive fetch failed'}
|
||||
$callback={param($user,$port,$identity,$known)
|
||||
& (Join-Path $tools 'invoke-provider-route.ps1') -SourceRoot $sourceRoot -SourceCommit $env:QUALIFICATION_SOURCE_SHA -Username $user -Port $port -IdentityFile $identity -KnownHosts $known -ReceiptRoot "$env:RUNNER_TEMP\arm-provider-route" -CandidateReceipt $env:CANDIDATE_RECEIPT -CandidateReceiptSha256 $env:CANDIDATE_RECEIPT_SHA256
|
||||
}.GetNewClosure()
|
||||
& (Join-Path $tools 'preview-ssh/prove-preview-openssh.ps1') -Archive $archive -Receipt "$env:RUNNER_TEMP\arm-provider-server.json" -ProductionRouteProbe $callback 2>&1 | Tee-Object .build/ssh-provider-receipts/native-route.log
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: stable-bun-arm-ssh-provider-receipts
|
||||
path: |
|
||||
.build/ssh-provider-receipts/
|
||||
.build/producer/results/
|
||||
.build/producer/work/source-cache-receipt.json
|
||||
.build/producer/producer-run.json
|
||||
${{ runner.temp }}/arm-provider-server.json
|
||||
${{ runner.temp }}/arm-provider-route/production-route.json
|
||||
${{ runner.temp }}/arm-provider-route/repaint-events.json
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,54 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SourceRoot,
|
||||
[Parameter(Mandatory=$true)][string]$SourceCommit,
|
||||
[Parameter(Mandatory=$true)][string]$Username,
|
||||
[Parameter(Mandatory=$true)][int]$Port,
|
||||
[Parameter(Mandatory=$true)][string]$IdentityFile,
|
||||
[Parameter(Mandatory=$true)][string]$KnownHosts,
|
||||
[Parameter(Mandatory=$true)][string]$ReceiptRoot,
|
||||
[Parameter(Mandatory=$true)][string]$CandidateReceipt,
|
||||
[Parameter(Mandatory=$true)][string]$CandidateReceiptSha256
|
||||
)
|
||||
$ErrorActionPreference='Stop'
|
||||
if($env:GITHUB_ACTIONS -ne 'true' -or $env:ORCA_ISOLATED_SSH_CI -ne '1'){throw 'Disposable CI only'}
|
||||
if($SourceCommit -notmatch '^[a-f0-9]{40}$'){throw 'Exact source hash required'}
|
||||
Push-Location $SourceRoot
|
||||
$knownPath=Join-Path ([Environment]::GetFolderPath('UserProfile')) '.ssh\known_hosts'
|
||||
$priorKnown=$null
|
||||
$copiedPaths=[Collections.Generic.List[string]]::new()
|
||||
$priorBackground=$env:ORCA_BACKGROUND_LAUNCH
|
||||
$knownExisted=Test-Path -LiteralPath $knownPath
|
||||
if($knownExisted){$priorKnown=[IO.File]::ReadAllBytes($knownPath)}
|
||||
try {
|
||||
$observed=(& git rev-parse HEAD).Trim()
|
||||
if($LASTEXITCODE -ne 0 -or $observed -ne $SourceCommit){throw 'Source checkout mismatch'}
|
||||
if(!(Test-Path 'out\relay\win32-arm64\relay.js')){throw 'Build real relay artifacts before server provisioning'}
|
||||
New-Item -ItemType Directory -Path $ReceiptRoot -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path (Split-Path $knownPath) -Force | Out-Null
|
||||
$pinned=[IO.File]::ReadAllText($KnownHosts)
|
||||
if($pinned -notmatch [regex]::Escape("[127.0.0.1]:$Port")){throw 'Missing private endpoint host-key pin'}
|
||||
Add-Content -LiteralPath $knownPath -Value "`n$pinned"
|
||||
if($CandidateReceiptSha256 -notmatch '^[a-f0-9]{64}$' -or (Get-FileHash -Algorithm SHA256 -LiteralPath $CandidateReceipt).Hash.ToLowerInvariant() -ne $CandidateReceiptSha256){throw 'Preverified candidate admission receipt mismatch'}
|
||||
$env:ORCA_BACKGROUND_LAUNCH='1'
|
||||
$env:ORCA_SSH_PROBE_STATE=Join-Path $ReceiptRoot 'state'
|
||||
New-Item -ItemType Directory -Path $env:ORCA_SSH_PROBE_STATE -Force | Out-Null
|
||||
$env:ORCA_RELAY_PATH=Join-Path $SourceRoot 'out\relay'
|
||||
$env:ORCA_SSH_PROBE_CONFIG=Join-Path $ReceiptRoot 'config.json'
|
||||
@{sourceCommit=$SourceCommit;observedSourceCommit=$observed;username=$Username;port=$Port;identityFile=$IdentityFile;candidateReceiptPath=$CandidateReceipt;candidateReceiptSha256=$CandidateReceiptSha256;receiptPath=(Join-Path $ReceiptRoot 'production-route.json')} | ConvertTo-Json | Set-Content $env:ORCA_SSH_PROBE_CONFIG
|
||||
foreach($name in @('windows-arm-provider-route.test.ts','windows-provider-candidate.ts','windows-provider-repaint.ts','windows-provider-loaded-images.ts')) {
|
||||
$destination=Join-Path $SourceRoot "src\main\ssh\$name"
|
||||
if(Test-Path -LiteralPath $destination){throw 'Diagnostic source destination already exists'}
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot $name) -Destination $destination
|
||||
$copiedPaths.Add($destination)
|
||||
}
|
||||
& node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/ssh/windows-arm-provider-route.test.ts --no-file-parallelism --reporter=verbose
|
||||
if($LASTEXITCODE -ne 0){throw 'Production SSH route qualification failed'}
|
||||
$result=Get-Content (Join-Path $ReceiptRoot 'production-route.json') -Raw | ConvertFrom-Json
|
||||
if(!$result.sameShellState -or !$result.cleanupVerified -or !$result.sourcePinRestored -or !$result.candidateAdmission.candidateOverride -or $result.repaint.koreanRows -ne 8 -or $result.repaint.latinRows -ne 8 -or !$result.repaint.exactRowsOnly -or !$result.repaint.provider.modules){throw 'Provider route cleanup/evidence incomplete'}
|
||||
} finally {
|
||||
foreach($destination in $copiedPaths) { Remove-Item -LiteralPath $destination -Force }
|
||||
$env:ORCA_BACKGROUND_LAUNCH=$priorBackground
|
||||
if($knownExisted){[IO.File]::WriteAllBytes($knownPath,$priorKnown)}else{Remove-Item -LiteralPath $knownPath -Force -ErrorAction SilentlyContinue}
|
||||
Remove-Item Env:ORCA_SSH_PROBE_CONFIG,Env:ORCA_SSH_PROBE_STATE,Env:ORCA_RELAY_PATH -ErrorAction SilentlyContinue
|
||||
Pop-Location
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"release": "10.0.0.0p2-Preview",
|
||||
"archiveSha256": "698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42",
|
||||
"files": [
|
||||
{
|
||||
"name": "libcrypto.dll",
|
||||
"sha256": "7ad2b7721893c54ad6e4fec1a3477701fb48975323c2c4ac6cd0b8c972ab242a",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10288
|
||||
},
|
||||
{
|
||||
"name": "scp.exe",
|
||||
"sha256": "53115de3294c52a3a2cc8b87a29ff71daa42cd72c6783ded3f12eac8e92662c1",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10312
|
||||
},
|
||||
{
|
||||
"name": "sftp-server.exe",
|
||||
"sha256": "f3a2f3b27094ec12518eafdcc39d1cdfd6f1550eac5c24850314a1a776783b8b",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10312
|
||||
},
|
||||
{
|
||||
"name": "sftp.exe",
|
||||
"sha256": "5e5560d2acb920e84f15762680be8542104ad58c508ce1d1ecca43408b26274d",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10272
|
||||
},
|
||||
{
|
||||
"name": "ssh-add.exe",
|
||||
"sha256": "64ec90bd7bfff3f95720a2af2d954026d47eddcb91b1a9af8a62734d0319c63b",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10296
|
||||
},
|
||||
{
|
||||
"name": "ssh-agent.exe",
|
||||
"sha256": "4dabe24c9439aefb024610112aac6866990be5165ffd1a2f8b8281af2e458064",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10272
|
||||
},
|
||||
{
|
||||
"name": "ssh-keygen.exe",
|
||||
"sha256": "c94940e4ea52fb073e460532884e0c14202e631db1d488a3a681d8230d57e0d6",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10312
|
||||
},
|
||||
{
|
||||
"name": "ssh-keyscan.exe",
|
||||
"sha256": "e68635da703812670aacf762a474191fe6cc25929bac5a498b3ea4c08667a79d",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10272
|
||||
},
|
||||
{
|
||||
"name": "ssh-pkcs11-helper.exe",
|
||||
"sha256": "95db4da86676ffe4595a037d356d01755f4f6a4267049fdda0b0c9f97a97d2c9",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10312
|
||||
},
|
||||
{
|
||||
"name": "ssh-shellhost.exe",
|
||||
"sha256": "d89f9a420268120789cf9dc1b94ef4313da1a258c3c57c172eda2fe999ded7e4",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10312
|
||||
},
|
||||
{
|
||||
"name": "ssh-sk-helper.exe",
|
||||
"sha256": "b563dfb6ee18b3b761cae0bfde212295e7fec5c6b9fb67ea1b19fc424bb805e4",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10296
|
||||
},
|
||||
{
|
||||
"name": "ssh.exe",
|
||||
"sha256": "fd87ccdfbd8be33d22b67fa3f1c94bb2327a3e7d24355bf7fd2485d356f1976d",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10296
|
||||
},
|
||||
{
|
||||
"name": "sshd-auth.exe",
|
||||
"sha256": "0c7ca28ed3649ef6f0ce1b3698cc7a92f51b86286fc077d5513f1bf5e48b178f",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10272
|
||||
},
|
||||
{
|
||||
"name": "sshd-session.exe",
|
||||
"sha256": "9f368188f703bd39594abefc29f28df5e97664acb937a1d26288b500ece4d463",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10272
|
||||
},
|
||||
{
|
||||
"name": "sshd.exe",
|
||||
"sha256": "f3eb3230d454dc662d5c5f09eface5acdeb2b196ccdd41b562644433a1562906",
|
||||
"machine": "0xAA64",
|
||||
"certificateTableBytes": 10312
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
# Ephemeral CI only. Preview ZIP server qualification, not inbox capability coverage.
|
||||
param([Parameter(Mandatory=$true)][string]$Receipt,[Parameter(Mandatory=$true)][string]$Archive,[scriptblock]$ProductionRouteProbe)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$report = @{scope='Microsoft Win32-OpenSSH 10.0.0.0p2-Preview ARM64 private loopback authentication and stock cmd.exe dispatch; NOT inbox server or relay deployment'; status='running'; imageVersion=$env:ImageVersion; cleanup=@('not-confirmed'); globalBootstrapCleanup='Not qualified: service bootstrap may create ProgramData SSH and OpenSSH registry entries; disposable CI VM destruction is the boundary'; observations=@(); stages=@(); diagnosticCaptureFailures=@()}
|
||||
$script:receiptWritten=$false
|
||||
function Write-Stage([string]$Stage) {
|
||||
$timestamp=[DateTime]::UtcNow.ToString('o')
|
||||
$report.stages += @{stage=$Stage; utc=$timestamp}
|
||||
try {
|
||||
$bytes=[Text.UTF8Encoding]::new($false).GetBytes(($report | ConvertTo-Json -Depth 6))
|
||||
$temporary="$Receipt.pending"
|
||||
$stream=[IO.FileStream]::new($temporary,[IO.FileMode]::Create,[IO.FileAccess]::Write,[IO.FileShare]::Read)
|
||||
try {$stream.Write($bytes,0,$bytes.Length);$stream.Flush($true)} finally {$stream.Dispose()}
|
||||
[IO.File]::Move($temporary,$Receipt,$true)
|
||||
$script:receiptWritten=$true
|
||||
} catch {Write-Warning 'Progress receipt could not be updated; cleanup must still run'}
|
||||
Write-Host "Native SSH stage: $Stage ($timestamp)"
|
||||
}
|
||||
Write-Stage 'preflight-start'
|
||||
if(-not $script:receiptWritten){throw 'Initial progress receipt unavailable; refuse provisioning'}
|
||||
if ($env:GITHUB_ACTIONS -ne 'true' -or $env:ORCA_ISOLATED_SSH_CI -ne '1' -or [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ne 'Arm64') { throw 'Requires isolated native ARM64 GitHub runner' }
|
||||
$admin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $admin) { throw 'Administrative private service/account setup required' }
|
||||
Write-Stage 'existing-server-query-start'
|
||||
if(Get-Service sshd -ErrorAction SilentlyContinue){throw 'Refuse an existing global SSH server'}
|
||||
Write-Stage 'existing-server-query-complete'
|
||||
Write-Stage 'default-shell-query-start'
|
||||
$registry = Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\OpenSSH' -ErrorAction SilentlyContinue
|
||||
if ($registry.DefaultShell -or $registry.DefaultShellCommandOption) { throw 'Requires stock cmd.exe OpenSSH shell; never rewrite registry' }
|
||||
Write-Stage 'default-shell-query-complete'
|
||||
$id = [Guid]::NewGuid().ToString('N').Substring(0,10)
|
||||
$name = "orca$id"
|
||||
$serviceName = "orca-sshd-$id"
|
||||
$root = Join-Path $env:RUNNER_TEMP "ossh-$id"
|
||||
Write-Stage 'private-directory-create-start'
|
||||
New-Item -ItemType Directory -Path $root | Out-Null
|
||||
Write-Stage 'private-directory-create-complete'
|
||||
$report.root=$root
|
||||
$createdUser=$false; $createdService=$false; $sid=$null; $ownedServerPid=$null
|
||||
$sshDir=Join-Path $root 'OpenSSH-ARM64'
|
||||
$sshdLog=Join-Path $root 'private-sshd.log'
|
||||
$serviceStartAttempt=$null
|
||||
function Diagnostic-Categories([string]$Text) {
|
||||
$categories=@()
|
||||
foreach($category in @('connection established','remote protocol version','server host key','host key verification failed','offering public key','server accepts key','authenticated to','sending command','exit status','permission denied','connection closed','connection reset','bad permissions','unable to load host key','no hostkeys available','failed to create','fatal','userauth','accepted publickey','starting session','createprocess','logonuser')){
|
||||
if($Text.IndexOf($category,[StringComparison]::OrdinalIgnoreCase) -ge 0){$categories+=$category}
|
||||
}
|
||||
return $categories
|
||||
}
|
||||
function Diagnostic-ExitStatuses([string]$Text) {
|
||||
$bounded=$Text.Substring(0,[Math]::Min($Text.Length,16384))
|
||||
$matches=[regex]::Matches($bounded,'(?im)\b(?:exit status|exit code|error(?: code)?)\s*[:=]?\s*(-?\d{1,10})\b')
|
||||
return @($matches | Select-Object -First 8 | ForEach-Object {[long]$_.Groups[1].Value})
|
||||
}
|
||||
function Invoke-Bounded([string]$Program,[string[]]$Arguments,[int]$Seconds=20,[switch]$AllowFailure) {
|
||||
Write-Stage ('command-'+[IO.Path]::GetFileName($Program)+'-start')
|
||||
$start=[Diagnostics.ProcessStartInfo]::new($Program)
|
||||
$start.UseShellExecute=$false; $start.CreateNoWindow=$true
|
||||
$start.RedirectStandardOutput=$true; $start.RedirectStandardError=$true
|
||||
foreach($argument in $Arguments){$start.ArgumentList.Add($argument)}
|
||||
$process=[Diagnostics.Process]::new();$process.StartInfo=$start
|
||||
try {
|
||||
if(-not $process.Start()){throw 'Owned command failed to start'}
|
||||
$stdout=$process.StandardOutput.ReadToEndAsync();$stderr=$process.StandardError.ReadToEndAsync()
|
||||
$timedOut=-not $process.WaitForExit($Seconds*1000)
|
||||
if($timedOut){$process.Kill($true);if(-not $process.WaitForExit(5000)){throw 'Owned command kill unconfirmed'}}
|
||||
if(-not [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($stdout,$stderr),5000)){throw 'Owned command output drain deadline exceeded'}
|
||||
$output=$stdout.GetAwaiter().GetResult();$errorText=$stderr.GetAwaiter().GetResult()
|
||||
if($output.Length+$errorText.Length -gt 1048576){throw 'Owned command output limit exceeded'}
|
||||
if([IO.Path]::GetFileName($Program) -eq 'ssh.exe'){
|
||||
$report.sshClient=@{timedOut=$timedOut;exitCode=$process.ExitCode;stderrBytes=$errorText.Length;categories=@(Diagnostic-Categories $errorText);reportedExitStatuses=@(Diagnostic-ExitStatuses $errorText)}
|
||||
Write-Stage 'ssh-client-result'
|
||||
}
|
||||
if([IO.Path]::GetFileName($Program) -eq 'sftp.exe'){
|
||||
$report.sftpClient=@{timedOut=$timedOut;exitCode=$process.ExitCode;stderrBytes=$errorText.Length;categories=@(Diagnostic-Categories $errorText);reportedExitStatuses=@(Diagnostic-ExitStatuses $errorText)}
|
||||
Write-Stage 'sftp-client-result'
|
||||
}
|
||||
if($timedOut){throw 'Owned command deadline exceeded'}
|
||||
if($process.ExitCode -ne 0 -and -not $AllowFailure){throw "Owned command failed: $([IO.Path]::GetFileName($Program)) exit $($process.ExitCode)"}
|
||||
Write-Stage ('command-'+[IO.Path]::GetFileName($Program)+'-complete')
|
||||
return @{code=$process.ExitCode; stdout=$output}
|
||||
} finally {$process.Dispose()}
|
||||
}
|
||||
function Record-PrivateServiceDiagnostics([switch]$AfterStop) {
|
||||
Write-Stage 'private-service-diagnostics-start'
|
||||
$captureStage='service-query'
|
||||
try {
|
||||
$state=Get-CimInstance Win32_Service -Filter "Name='$serviceName'"
|
||||
if($state){
|
||||
$diagnostic=@{state=$state.State;exitCode=$state.ExitCode;serviceSpecificExitCode=$state.ServiceSpecificExitCode;pid=$state.ProcessId;localSystem=($state.StartName -eq 'LocalSystem');privatePath=($state.PathName -like "*$root*")}
|
||||
} else {$diagnostic=@{absent=$true}}
|
||||
if($AfterStop){$report.serviceAfterStop=$diagnostic}else{$report.serviceDiagnostics=$diagnostic}
|
||||
if($serviceStartAttempt -and -not $AfterStop){
|
||||
try {
|
||||
$events=@(Get-WinEvent -FilterHashtable @{LogName='System';ProviderName='Service Control Manager';StartTime=$serviceStartAttempt} -MaxEvents 50 -ErrorAction Stop | Where-Object {$_.Properties.Value -contains $serviceName})
|
||||
$report.serviceEvents=@($events | ForEach-Object {@{id=$_.Id;utc=$_.TimeCreated.ToUniversalTime().ToString('o');level=$_.Level}})
|
||||
} catch {$report.serviceEventsUnavailable=$true}
|
||||
}
|
||||
$captureStage='private-log'
|
||||
if(Test-Path -LiteralPath $sshdLog){
|
||||
$file=[IO.FileStream]::new($sshdLog,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::ReadWrite)
|
||||
try {
|
||||
$buffer=[byte[]]::new(16384)
|
||||
$file.Position=[Math]::Max(0,$file.Length-$buffer.Length)
|
||||
$offset=$file.Position
|
||||
$count=$file.Read($buffer,0,$buffer.Length)
|
||||
$text=[Text.Encoding]::UTF8.GetString($buffer,0,$count)
|
||||
$classes=@(Diagnostic-Categories $text)
|
||||
$report.privateLog=@{exists=$true;bytes=$file.Length;examinedBytes=$count;offset=$offset;errorClasses=$classes;reportedExitStatuses=@(Diagnostic-ExitStatuses $text)}
|
||||
} finally {$file.Dispose()}
|
||||
} else {$report.privateLog=@{exists=$false}}
|
||||
} catch {$report.diagnosticCaptureFailures+=@{stage=$captureStage;afterStop=[bool]$AfterStop;hresult=$_.Exception.HResult;kind=$_.Exception.GetType().Name}}
|
||||
Write-Stage 'private-service-diagnostics-complete'
|
||||
}
|
||||
|
||||
function Machine([string]$Path){
|
||||
$file=[IO.File]::OpenRead($Path)
|
||||
try{$reader=[IO.BinaryReader]::new($file);$file.Position=0x3c;$position=$reader.ReadInt32();$file.Position=$position;if($reader.ReadUInt32()-ne 0x00004550){throw 'Invalid PE'};return ('0x{0:X4}'-f $reader.ReadUInt16())}finally{$file.Dispose()}
|
||||
}
|
||||
try {
|
||||
Write-Stage 'preview-archive-verify-start'
|
||||
$expectedArchive='698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42'
|
||||
if((Get-FileHash -LiteralPath $Archive -Algorithm SHA256).Hash.ToLowerInvariant() -ne $expectedArchive){throw 'Preview archive hash mismatch'}
|
||||
$report.archiveSha256=$expectedArchive
|
||||
Write-Stage 'preview-archive-verify-complete'
|
||||
Write-Stage 'preview-extract-start'
|
||||
# The exact hash is checked before extraction; never execute included installer scripts.
|
||||
[IO.Compression.ZipFile]::ExtractToDirectory($Archive,$root)
|
||||
Write-Stage 'preview-extract-complete'
|
||||
Write-Stage 'preview-native-input-verification-start'
|
||||
$manifest=Get-Content -LiteralPath (Join-Path $PSScriptRoot 'preview-native-inputs.json') -Raw | ConvertFrom-Json
|
||||
if($manifest.archiveSha256 -ne $expectedArchive -or $manifest.files.Count -ne 15){throw 'Preview input manifest mismatch'}
|
||||
$nativeFiles=@(Get-ChildItem -LiteralPath $sshDir -File | Where-Object {$_.Extension -in @('.exe','.dll')})
|
||||
if($nativeFiles.Count -ne $manifest.files.Count){throw 'Unexpected preview native input count'}
|
||||
$verified=@()
|
||||
foreach($file in $nativeFiles){
|
||||
$expected=@($manifest.files | Where-Object name -eq $file.Name)
|
||||
if($expected.Count -ne 1 -or (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant() -ne $expected[0].sha256){throw 'Preview native input hash mismatch'}
|
||||
if((Machine $file.FullName) -ne '0xAA64'){throw 'Preview native input is not ARM64'}
|
||||
$signature=Get-AuthenticodeSignature -LiteralPath $file.FullName
|
||||
if($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notmatch '(?:^|, )O=Microsoft Corporation(?:,|$)'){throw 'Preview native input Microsoft signature invalid'}
|
||||
$verified+=@{name=$file.Name;sha256=$expected[0].sha256;machine='0xAA64';signature='Valid';publisher=$signature.SignerCertificate.Subject}
|
||||
}
|
||||
$report.nativeInputs=$verified
|
||||
Write-Stage 'preview-native-input-verification-complete'
|
||||
$sshd=Join-Path $sshDir 'sshd.exe';$ssh=Join-Path $sshDir 'ssh.exe';$keygen=Join-Path $sshDir 'ssh-keygen.exe'
|
||||
$password=ConvertTo-SecureString ([Guid]::NewGuid().ToString('N')+'aA!7') -AsPlainText -Force
|
||||
Write-Stage 'private-user-collision-query-start'
|
||||
if(Get-LocalUser -Name $name -ErrorAction SilentlyContinue){throw 'Private username collision'}
|
||||
Write-Stage 'private-user-collision-query-complete'
|
||||
$createdUser=$true
|
||||
Write-Stage 'private-user-create-start'
|
||||
$user=New-LocalUser -Name $name -Password $password -AccountNeverExpires -PasswordNeverExpires -Description 'Ephemeral Orca SSH qualification'
|
||||
Write-Stage 'private-user-create-complete'
|
||||
$sid=$user.SID.Value
|
||||
Write-Stage 'private-user-group-start'
|
||||
Add-LocalGroupMember -SID 'S-1-5-32-545' -Member $user
|
||||
Write-Stage 'private-user-group-complete'
|
||||
# No administrator membership, real runner auth files or global DefaultShell modifications.
|
||||
Invoke-Bounded icacls.exe @($root,'/inheritance:r','/grant:r','*S-1-5-18:(OI)(CI)F','*S-1-5-32-544:(OI)(CI)F',"*$($sid):(RX)") | Out-Null
|
||||
# /T visits files too: grant direct rights instead of directory-only inheritance flags.
|
||||
$runnerSid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
||||
Invoke-Bounded icacls.exe @($sshDir,'/inheritance:r','/grant:r','*S-1-5-18:F','*S-1-5-32-544:F',"*$($runnerSid):F","*$($sid):RX",'/T') | Out-Null
|
||||
$report.nativeAcl=@{directory=(Get-Acl -LiteralPath $sshDir).Sddl;keygen=(Get-Acl -LiteralPath $keygen).Sddl}
|
||||
Write-Stage 'native-acl-recorded'
|
||||
$hostKey=Join-Path $root 'host_key';$clientKey=Join-Path $root 'client_key'
|
||||
Write-Stage 'private-key-create-start'
|
||||
Invoke-Bounded $keygen @('-q','-t','ed25519','-N','','-f',$hostKey) | Out-Null
|
||||
Invoke-Bounded $keygen @('-q','-t','ed25519','-N','','-f',$clientKey) | Out-Null
|
||||
Write-Stage 'private-key-create-complete'
|
||||
# Service host keys are readable only by SYSTEM and administrators.
|
||||
Invoke-Bounded icacls.exe @($hostKey,'/inheritance:r','/grant:r','*S-1-5-18:F','*S-1-5-32-544:F') | Out-Null
|
||||
Invoke-Bounded icacls.exe @($hostKey,'/setowner','*S-1-5-18') | Out-Null
|
||||
$hostAcl=Get-Acl -LiteralPath $hostKey
|
||||
$hostAcl.SetSecurityDescriptorSddlForm('D:P(A;;FA;;;SY)(A;;FA;;;BA)',[Security.AccessControl.AccessControlSections]::Access)
|
||||
Set-Acl -LiteralPath $hostKey -AclObject $hostAcl
|
||||
$report.hostKeyAcl=(Get-Acl -LiteralPath $hostKey).Sddl
|
||||
$authorized=Join-Path $root 'authorized_keys'
|
||||
Copy-Item -LiteralPath "$clientKey.pub" -Destination $authorized
|
||||
Invoke-Bounded icacls.exe @($authorized,'/inheritance:r','/grant:r','*S-1-5-18:F','*S-1-5-32-544:F',"*$($sid):R") | Out-Null
|
||||
$listener=[Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback,0);$listener.Start();$port=$listener.LocalEndpoint.Port;$listener.Stop()
|
||||
$config=Join-Path $root 'sshd_config'
|
||||
$hostPosix=$hostKey.Replace('\','/');$authPosix=$authorized.Replace('\','/');$pidPosix=(Join-Path $root 'sshd.pid').Replace('\','/')
|
||||
$sftpServer=Join-Path $sshDir 'sftp-server.exe'
|
||||
$sftpServerPosix=$sftpServer.Replace('\','/')
|
||||
$report.sftpServer=@{pinnedPath=$true;sha256=(Get-FileHash -LiteralPath $sftpServer -Algorithm SHA256).Hash.ToLowerInvariant();acl=(Get-Acl -LiteralPath $sftpServer).Sddl}
|
||||
@"
|
||||
Port $port
|
||||
ListenAddress 127.0.0.1
|
||||
HostKey "$hostPosix"
|
||||
PidFile "$pidPosix"
|
||||
AuthorizedKeysFile "$authPosix"
|
||||
AllowUsers $name
|
||||
PubkeyAuthentication yes
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
StrictModes yes
|
||||
AllowTcpForwarding no
|
||||
AllowAgentForwarding no
|
||||
PermitTunnel no
|
||||
PermitTTY no
|
||||
Subsystem sftp "$sftpServerPosix"
|
||||
LogLevel DEBUG1
|
||||
"@ | Set-Content -LiteralPath $config -Encoding ascii
|
||||
Write-Stage 'server-config-validate-start'
|
||||
Invoke-Bounded $sshd @('-t','-f',$config) | Out-Null
|
||||
Write-Stage 'server-config-validate-complete'
|
||||
# A distinct LocalSystem service supplies Windows sshd's token-creation privileges.
|
||||
Write-Stage 'private-service-collision-query-start'
|
||||
if(Get-Service -Name $serviceName -ErrorAction SilentlyContinue){throw 'Private service name collision'}
|
||||
Write-Stage 'private-service-collision-query-complete'
|
||||
$createdService=$true
|
||||
Write-Stage 'private-service-create-start'
|
||||
New-Service -Name $serviceName -BinaryPathName "`"$sshd`" -f `"$config`" -E `"$sshdLog`"" -StartupType Manual | Out-Null
|
||||
Write-Stage 'private-service-create-complete'
|
||||
Invoke-Bounded sc.exe @('privs',$serviceName,'SeAssignPrimaryTokenPrivilege/SeTcbPrivilege/SeBackupPrivilege/SeRestorePrivilege/SeImpersonatePrivilege') | Out-Null
|
||||
Write-Stage 'private-service-start-start'
|
||||
$serviceStartAttempt=[DateTime]::Now.AddSeconds(-1)
|
||||
Start-Service -Name $serviceName
|
||||
Write-Stage 'private-service-start-complete'
|
||||
Write-Stage 'private-service-identity-start'
|
||||
$service=Get-CimInstance Win32_Service -Filter "Name='$serviceName'"
|
||||
Write-Stage 'private-service-identity-complete'
|
||||
if($service.StartName -ne 'LocalSystem' -or -not $service.ProcessId){throw 'Private service identity unavailable'}
|
||||
$ownedServerPid=$service.ProcessId
|
||||
$keyFields=(Get-Content -LiteralPath "$hostKey.pub" -Raw).Trim().Split(' ')
|
||||
$known=Join-Path $root 'known_hosts'
|
||||
"[127.0.0.1]:$port $($keyFields[0]) $($keyFields[1])" | Set-Content -LiteralPath $known -Encoding ascii
|
||||
$nonce=[Guid]::NewGuid().ToString('N')
|
||||
$sshArgs=@('-v','-F','NUL','-T','-p',[string]$port,'-i',$clientKey,'-o','BatchMode=yes','-o','IdentitiesOnly=yes','-o','StrictHostKeyChecking=yes','-o',"UserKnownHostsFile=$known",'-o','ConnectTimeout=5',"$name@127.0.0.1")
|
||||
$deadline=[DateTime]::UtcNow.AddSeconds(75);$probe=$null
|
||||
$report.sshFirstLoginBudgetSeconds=60
|
||||
Write-Stage 'ssh-authentication-start'
|
||||
do {
|
||||
$remainingSeconds=[Math]::Max(1,[Math]::Min(60,[Math]::Floor(($deadline-[DateTime]::UtcNow).TotalSeconds)))
|
||||
$attemptClock=[Diagnostics.Stopwatch]::StartNew()
|
||||
try {$probe=Invoke-Bounded $ssh ($sshArgs+@("echo $nonce && whoami && echo %COMSPEC%")) $remainingSeconds -AllowFailure}
|
||||
finally {$report.sshAttemptElapsedMs=$attemptClock.ElapsedMilliseconds;Write-Stage 'ssh-attempt-finished'}
|
||||
if($probe.code -eq 0){break};Start-Sleep -Milliseconds 250
|
||||
} while([DateTime]::UtcNow -lt $deadline)
|
||||
if($probe.code -ne 0 -or $probe.stdout -notmatch [regex]::Escape($nonce) -or $probe.stdout -notmatch "\\$name(?:\r?\n)" -or $probe.stdout -notmatch '(?i)cmd.exe'){throw 'Real SSH authentication/default-shell proof failed'}
|
||||
Write-Stage 'ssh-authentication-complete'
|
||||
Write-Stage 'listener-identity-start'
|
||||
$listeners=@(Get-NetTCPConnection -State Listen -LocalPort $port)
|
||||
Write-Stage 'listener-identity-complete'
|
||||
if(-not $listeners -or @($listeners|Where-Object {$_.LocalAddress -ne '127.0.0.1' -or $_.OwningProcess -ne $ownedServerPid}).Count){throw 'Listener escaped private loopback owner'}
|
||||
$report.observations=@{serverMachine=(Machine $sshd);clientMachine=(Machine $ssh);publisherVerified=$true;serviceAccount='LocalSystem';dedicatedUser=$true;pinnedHostKey=$true;stockCmdDispatch=$true;loopbackOnly=$true;port=$port;servicePid=$ownedServerPid}
|
||||
if ($ProductionRouteProbe) {
|
||||
Write-Stage 'sftp-preflight-start'
|
||||
$sftpBatch=Join-Path $root 'sftp-probe.txt'
|
||||
"pwd`nquit" | Set-Content -LiteralPath $sftpBatch -Encoding ascii
|
||||
$sftp=Join-Path $sshDir 'sftp.exe'
|
||||
$sftpArgs=@('-v','-S',$ssh,'-F','NUL','-P',[string]$port,'-i',$clientKey,'-b',$sftpBatch,'-o','BatchMode=yes','-o','IdentitiesOnly=yes','-o','StrictHostKeyChecking=yes','-o',"UserKnownHostsFile=$known",'-o','ConnectTimeout=5',"$name@127.0.0.1")
|
||||
$sftpProof=Invoke-Bounded $sftp $sftpArgs 30 -AllowFailure
|
||||
if($sftpProof.code -ne 0){throw 'Pinned native SFTP subsystem preflight failed'}
|
||||
$report.sftpSubsystem=@{implementation='pinned external sftp-server.exe';authenticatedBatchPassed=$true}
|
||||
Write-Stage 'sftp-preflight-complete'
|
||||
Write-Stage 'production-route-start'
|
||||
& $ProductionRouteProbe $name $port $clientKey $known
|
||||
$report.productionRoute='passed-authenticated-cleanup'
|
||||
Write-Stage 'production-route-complete'
|
||||
}
|
||||
$report.status='proof-passed-cleanup-pending'
|
||||
} catch {
|
||||
$report.status='failed';$report.error=$_.Exception.Message
|
||||
} finally {
|
||||
try {
|
||||
Record-PrivateServiceDiagnostics
|
||||
Write-Stage 'cleanup-start'
|
||||
Write-Stage 'cleanup-service-query-start'
|
||||
$privateService=Get-CimInstance Win32_Service -Filter "Name='$serviceName'"
|
||||
Write-Stage 'cleanup-service-query-complete'
|
||||
if($createdService -and $privateService -and ($privateService.PathName -notlike "*$root*" -or ($ownedServerPid -and $privateService.ProcessId -and $privateService.ProcessId -ne $ownedServerPid))){throw 'Private service identity changed; refuse stop'}
|
||||
Write-Stage 'cleanup-child-accounting-start'
|
||||
$rows=@(Get-CimInstance Win32_Process)
|
||||
$ownedChildren=@($rows | Where-Object {$_.ExecutablePath -and $_.ExecutablePath.StartsWith($sshDir+'\',[StringComparison]::OrdinalIgnoreCase)})
|
||||
$report.childrenBeforeStop=@($ownedChildren | ForEach-Object {@{pid=$_.ProcessId;parentPid=$_.ParentProcessId;created=$_.CreationDate.ToUniversalTime().ToString('o');image=[IO.Path]::GetFileName($_.ExecutablePath)}})
|
||||
Write-Stage 'cleanup-child-accounting-complete'
|
||||
Write-Stage 'cleanup-service-stop-delete-start'
|
||||
if($createdService){Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue;Invoke-Bounded sc.exe @('delete',$serviceName) | Out-Null}
|
||||
Write-Stage 'cleanup-service-stop-delete-complete'
|
||||
Write-Stage 'cleanup-process-exit-start'
|
||||
$exitDeadline=[DateTime]::UtcNow.AddSeconds(10)
|
||||
while($ownedServerPid -and (Get-Process -Id $ownedServerPid -ErrorAction SilentlyContinue) -and [DateTime]::UtcNow -lt $exitDeadline){Start-Sleep -Milliseconds 100}
|
||||
if($ownedServerPid -and (Get-Process -Id $ownedServerPid -ErrorAction SilentlyContinue)){throw 'Private sshd process still live; no PID-only kill attempted'}
|
||||
Write-Stage 'cleanup-process-exit-complete'
|
||||
Write-Stage 'cleanup-service-absence-start'
|
||||
$serviceDeadline=[DateTime]::UtcNow.AddSeconds(10)
|
||||
while($createdService -and (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) -and [DateTime]::UtcNow -lt $serviceDeadline){Start-Sleep -Milliseconds 100}
|
||||
if($createdService -and (Get-Service -Name $serviceName -ErrorAction SilentlyContinue)){throw 'Private service still registered'}
|
||||
Write-Stage 'cleanup-service-absence-complete'
|
||||
Record-PrivateServiceDiagnostics -AfterStop
|
||||
Write-Stage 'cleanup-child-exit-start'
|
||||
$childDeadline=[DateTime]::UtcNow.AddSeconds(10)
|
||||
do {
|
||||
$remaining=@(Get-CimInstance Win32_Process | Where-Object {$_.ExecutablePath -and $_.ExecutablePath.StartsWith($sshDir+'\',[StringComparison]::OrdinalIgnoreCase)})
|
||||
if(-not $remaining.Count){break};Start-Sleep -Milliseconds 200
|
||||
} while([DateTime]::UtcNow -lt $childDeadline)
|
||||
$report.childrenAfterStop=@($remaining | ForEach-Object {@{pid=$_.ProcessId;parentPid=$_.ParentProcessId;created=$_.CreationDate.ToUniversalTime().ToString('o');image=[IO.Path]::GetFileName($_.ExecutablePath)}})
|
||||
Write-Stage 'cleanup-child-exit-complete'
|
||||
if($remaining.Count){throw 'Private SSH child processes remain; preserve files and discard ephemeral runner'}
|
||||
Write-Stage 'cleanup-user-profile-start'
|
||||
if($sid){
|
||||
$profileWait=[Diagnostics.Stopwatch]::StartNew()
|
||||
do {
|
||||
$profiles=@(Get-CimInstance Win32_UserProfile | Where-Object SID -eq $sid)
|
||||
if(-not @($profiles | Where-Object Loaded).Count){break}
|
||||
Start-Sleep -Milliseconds 500
|
||||
} while($profileWait.Elapsed.TotalSeconds -lt 30)
|
||||
$report.profileUnloadWaitMs=$profileWait.ElapsedMilliseconds
|
||||
$report.privateProfile=@($profiles | ForEach-Object {@{loaded=$_.Loaded;status=$_.Status}})
|
||||
Write-Stage 'cleanup-user-profile-observed'
|
||||
$loadedProfiles=@($profiles | Where-Object Loaded)
|
||||
$report.profileCleanup=if($loadedProfiles.Count){'Loaded profile retained for disposable CI VM destruction'}else{'Unloaded profile removed'}
|
||||
$profiles | Where-Object {-not $_.Loaded} | Remove-CimInstance
|
||||
}
|
||||
Write-Stage 'cleanup-user-profile-complete'
|
||||
Write-Stage 'cleanup-user-start'
|
||||
if($createdUser){Remove-LocalUser -Name $name;if(Get-LocalUser -Name $name -ErrorAction SilentlyContinue){throw 'Private account still exists'}}
|
||||
Write-Stage 'cleanup-user-complete'
|
||||
Write-Stage 'cleanup-private-files-start'
|
||||
Remove-Item -LiteralPath $root -Recurse -Force
|
||||
Write-Stage 'cleanup-private-files-complete'
|
||||
if($report.status -eq 'proof-passed-cleanup-pending'){$report.status='passed'}
|
||||
$report.cleanup=@('private service stopped/deleted','owned sshd exit verified','private account removed; profile disposition recorded separately','private keys removed')
|
||||
} catch {$report.status='failed';$report.cleanup=@('cleanup unverifiable; discard ephemeral runner');$report.cleanupError=$_.Exception.Message}
|
||||
Write-Stage 'finished'
|
||||
}
|
||||
if($report.status -ne 'passed'){throw 'Native OpenSSH qualification failed; inspect sanitized receipt'}
|
||||
@@ -0,0 +1,17 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$errors=$null;$tokens=$null
|
||||
$ast=[Management.Automation.Language.Parser]::ParseFile((Join-Path $PSScriptRoot 'prove-preview-openssh.ps1'),[ref]$tokens,[ref]$errors)
|
||||
if($errors.Count){throw 'Fixture failed to parse'}
|
||||
$definition=$ast.Find({param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Diagnostic-ExitStatuses'},$true)
|
||||
. ([scriptblock]::Create($definition.Extent.Text))
|
||||
function Assert-Statuses([string]$Text,[long[]]$Expected){
|
||||
$actual=@(Diagnostic-ExitStatuses $Text)
|
||||
if(($actual -join ',') -ne ($Expected -join ',')){throw 'Unexpected numeric diagnostic'}
|
||||
}
|
||||
Assert-Statuses "debug1: Exit status 3221225781`nclient secret path /private/id" @(3221225781)
|
||||
Assert-Statuses 'CreateProcess error: 5; exit code -1073741515' @(5,-1073741515)
|
||||
Assert-Statuses 'identity key-123, host 127.0.0.1 port 65000' @()
|
||||
Assert-Statuses ('x'*16384+' exit status 123') @()
|
||||
Assert-Statuses (('exit status 7; '*20)) @(7,7,7,7,7,7,7,7)
|
||||
# Extract functions through the AST: never provision the fixture while testing diagnostics.
|
||||
'PASS: fixture parse and five numeric-diagnostic cases'
|
||||
@@ -0,0 +1,108 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createRequire } from 'node:module'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve, join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
export const SOURCE = '744846f844374847c902b5e7fd59b4342a51ef99'
|
||||
export const PRODUCER = '34d1c11c67cbd1a653da1d07796daf7a1b0f2a7d'
|
||||
export const PRODUCT = '2084c58ba5410106ce61153a9fb16cdb4b6e5301'
|
||||
export const PATCH = '276f475c90c6761c58b9f56b3f4bfafa079d0c29c5861b23d2320c9ff39c35fb'
|
||||
export const SUCCESS = 'Both patched Bun Windows targets built with network disabled. Native Windows behavior, signatures and production runtime promotion remain unqualified.'
|
||||
const digest = file => createHash('sha256').update(readFileSync(file)).digest('hex')
|
||||
const json = file => JSON.parse(readFileSync(file, 'utf8'))
|
||||
|
||||
export function verifyProducer(run, runId) {
|
||||
assert.equal(String(run.id), String(runId))
|
||||
assert.equal(run.repository?.full_name, 'stablyai/orca')
|
||||
assert.equal(run.status, 'completed')
|
||||
assert.equal(run.conclusion, 'success')
|
||||
assert.equal(run.head_sha, PRODUCER)
|
||||
assert.equal(run.head_branch, 'OrcaWin/np-full-offline-bun-diagnostic')
|
||||
assert.equal(run.path, '.github/workflows/diagnostic-full-offline-bun.yml')
|
||||
assert.equal(run.event, 'push')
|
||||
}
|
||||
|
||||
export function verifyStableBuildOptions(root) {
|
||||
const receipts = {}
|
||||
for (const arch of ['x64', 'aarch64']) {
|
||||
const argsPath = join(root, 'results', `${arch}-args.txt`)
|
||||
const args = readFileSync(argsPath, 'utf8').trim().split(/\r?\n/)
|
||||
assert.deepEqual(args, ['--profile=release', '--canary=false', '--os=windows',
|
||||
`--arch=${arch}`, '--lto=off', `--build-dir=build/conpty-${arch}`, '-j2',
|
||||
...(arch === 'x64' ? ['--baseline=true'] : [])], 'Unexpected effective build arguments')
|
||||
const optionsPath = join(root, 'results', `${arch}-build-options.rs`)
|
||||
const options = readFileSync(optionsPath, 'utf8')
|
||||
for (const [name, declaration] of Object.entries({
|
||||
IS_CANARY: 'pub const IS_CANARY: bool = false;',
|
||||
SHA: `pub const SHA: &str = "${SOURCE}";`,
|
||||
BASE_PATH: 'pub const BASE_PATH: &[u8] = "/work/source".as_bytes();',
|
||||
CODEGEN_PATH: `pub const CODEGEN_PATH: &[u8] = "/work/source/build/conpty-${arch}/codegen".as_bytes();`
|
||||
})) {
|
||||
const matches = options.split(/\r?\n/).filter(line => line.includes(`pub const ${name}:`))
|
||||
assert.deepEqual(matches, [declaration], `Unexpected effective ${name}`)
|
||||
}
|
||||
assert.match(options, /pub const VERSION: crate::Version = crate::Version \{\r?\n major: 1,\r?\n minor: 4,\r?\n patch: 2,\r?\n\};/)
|
||||
receipts[arch] = { argsSha256: digest(argsPath), optionsSha256: digest(optionsPath) }
|
||||
}
|
||||
return receipts
|
||||
}
|
||||
|
||||
export function verifyOutput(root, productRoot, arch) {
|
||||
assert.ok(['x64', 'arm64'].includes(arch))
|
||||
assert.equal(readFileSync(join(root, 'results/SUCCESS'), 'utf8').trim(), SUCCESS)
|
||||
assert.equal(json(join(root, 'work/source-cache-receipt.json')).source, SOURCE)
|
||||
assert.equal(digest(join(root, 'results/applied.patch')), PATCH)
|
||||
const container = json(join(root, 'results/container.json'))
|
||||
assert.equal(container.length, 1)
|
||||
assert.equal(container[0].HostConfig.NetworkMode, 'none')
|
||||
assert.equal(container[0].HostConfig.NanoCpus, 2_000_000_000)
|
||||
assert.equal(container[0].HostConfig.Memory, 8 * 1024 ** 3)
|
||||
assert.equal(container[0].HostConfig.PidsLimit, 512)
|
||||
const buildOptions = verifyStableBuildOptions(root)
|
||||
const rows = readFileSync(join(root, 'results/output.sha256'), 'utf8').trim().split(/\r?\n/)
|
||||
assert.equal(rows.length, 2)
|
||||
const hashes = new Map()
|
||||
for (const row of rows) {
|
||||
const match = /^([a-f0-9]{64}) (bun-windows-(?:x64|aarch64)\.exe)$/.exec(row)
|
||||
assert.ok(match, 'Malformed output hash')
|
||||
assert.ok(!hashes.has(match[2]), 'Duplicate output hash')
|
||||
hashes.set(match[2], match[1])
|
||||
}
|
||||
const require = createRequire(join(productRoot, 'package.json'))
|
||||
const { readPeMachine, PE_MACHINE } = require('./config/scripts/windows-pe-machine.cjs')
|
||||
for (const [target, filename] of [['x64', 'bun-windows-x64.exe'], ['arm64', 'bun-windows-aarch64.exe']]) {
|
||||
const file = join(root, 'results', filename)
|
||||
assert.equal(digest(file), hashes.get(filename), 'Runtime digest mismatch')
|
||||
assert.equal(readPeMachine(file), PE_MACHINE[target], 'Runtime architecture mismatch')
|
||||
}
|
||||
const name = `bun-windows-${arch === 'arm64' ? 'aarch64' : arch}.exe`
|
||||
return { binary: resolve(root, 'results', name), sha256: hashes.get(name), architecture: arch, buildOptions }
|
||||
}
|
||||
|
||||
export async function verifyPackage(candidate, productRoot) {
|
||||
assert.equal(digest(join(productRoot, 'out/orcad/bun-runtime.exe')), candidate.sha256,
|
||||
'Package substituted another runtime')
|
||||
const { WINDOWS_CONPTY_FILES } = await import(pathToFileURL(join(productRoot, 'src/shared/windows-conpty-release.ts')))
|
||||
const require = createRequire(join(productRoot, 'package.json'))
|
||||
const { readPeMachine, PE_MACHINE } = require('./config/scripts/windows-pe-machine.cjs')
|
||||
const companions = {}
|
||||
for (const [name, expected] of Object.entries(WINDOWS_CONPTY_FILES[candidate.architecture])) {
|
||||
const file = join(productRoot, 'out/orcad/conpty', name)
|
||||
assert.equal(digest(file), expected, 'ConPTY digest mismatch')
|
||||
assert.equal(readPeMachine(file), PE_MACHINE[candidate.architecture])
|
||||
companions[name] = expected
|
||||
}
|
||||
return { ...candidate, companions, conptyLibrary: resolve(productRoot, 'out/orcad/conpty/conpty.dll') }
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
||||
const [mode, receiptRoot, productRoot, arch, outputFile, runId] = process.argv.slice(2)
|
||||
verifyProducer(json(join(receiptRoot, 'producer-run.json')), runId)
|
||||
const candidate = verifyOutput(receiptRoot, resolve(productRoot), arch)
|
||||
const result = mode === 'package' ? await verifyPackage(candidate, resolve(productRoot)) : candidate
|
||||
assert.ok(mode === 'candidate' || mode === 'package')
|
||||
writeFileSync(outputFile, JSON.stringify({ producerRun: runId, producer: PRODUCER, source: SOURCE,
|
||||
product: PRODUCT, patch: PATCH, ...result }, null, 2) + '\n')
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
import { createHash, randomUUID } from 'node:crypto'
|
||||
import { readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
|
||||
// Only Electron artifact discovery is substituted; SSH/deployment/leases stay real.
|
||||
vi.mock('electron', () => ({
|
||||
app: { getAppPath: () => process.cwd(), getPath: () => process.env.ORCA_SSH_PROBE_STATE }
|
||||
}))
|
||||
import { ORCAD_BUN_RELEASE_ASSETS } from '../../shared/orcad-bun-runtime'
|
||||
import { SshConnection } from './ssh-connection'
|
||||
import { decodeRemotePowerShellScript, powerShellCommand } from './ssh-remote-powershell'
|
||||
import { deployAndLaunchRelay } from './ssh-relay-deploy'
|
||||
import { SshChannelMultiplexer } from './ssh-channel-multiplexer'
|
||||
import { readWindowsProcessTableFresh } from '../windows/windows-process-table'
|
||||
import { installCandidateOverride } from './windows-provider-candidate'
|
||||
import { proveRepaint } from './windows-provider-repaint'
|
||||
import { inspectProviderImages } from './windows-provider-loaded-images'
|
||||
import { RELAY_WINDOWS_CONPTY_FILENAMES } from '../../shared/relay-artifacts'
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value))
|
||||
throw new Error('Invalid RPC object')
|
||||
return Object.fromEntries(Object.entries(value))
|
||||
}
|
||||
function textField(value: Record<string, unknown>, key: string): string {
|
||||
const text = value[key]
|
||||
if (typeof text !== 'string' || !text) throw new Error(`Missing ${key}`)
|
||||
return text
|
||||
}
|
||||
function diagnosticErrorText(output: string): string {
|
||||
const serialized = [...output.matchAll(/<S\b[^>]*\bS="Error"[^>]*>([^<]*)<\/S>/gu)]
|
||||
const text = output.trimStart().startsWith('#< CLIXML')
|
||||
? serialized
|
||||
.map((match) =>
|
||||
match[1]
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&/g, '&')
|
||||
.replace(/_x([0-9a-f]{4})_/gi, (_, hex: string) =>
|
||||
String.fromCharCode(Number.parseInt(hex, 16))
|
||||
)
|
||||
)
|
||||
.join('')
|
||||
: output
|
||||
// PowerShell source/location echoes contain command text, not failure evidence.
|
||||
return text
|
||||
.replace(/^[ \t]*\+[ \t]*(CategoryInfo|FullyQualifiedErrorId)[ \t]*:/gmu, '$1:')
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => !/^\s*(?:\+|At (?:line:|.+:line\s))/u.test(line))
|
||||
.join('\n')
|
||||
}
|
||||
function structuredCimFailure(output: string): Record<string, unknown> {
|
||||
const identifiers = [
|
||||
'MI RESULT 2',
|
||||
'MI RESULT 5',
|
||||
'MI RESULT 6',
|
||||
'MI RESULT 7',
|
||||
'HRESULT 0x80070005',
|
||||
'HRESULT 0x80041003',
|
||||
'AccessDenied',
|
||||
'PermissionDenied'
|
||||
] as const
|
||||
const identifier =
|
||||
/^\s*FullyQualifiedErrorId\s*:\s*([^,\r\n]+),Microsoft\.Management\.Infrastructure\.CimCmdlets\.InvokeCimMethodCommand\s*$/mu.exec(
|
||||
output
|
||||
)?.[1]
|
||||
const category =
|
||||
/^\s*CategoryInfo\s*:\s*(PermissionDenied|NotSpecified|InvalidOperation|ResourceUnavailable|ObjectNotFound)\s*:/mu.exec(
|
||||
output
|
||||
)?.[1]
|
||||
const identifierHresult = /^HRESULT (0x[0-9a-f]{8})$/iu.exec(identifier ?? '')?.[1]
|
||||
const codes: Record<string, number> = identifierHresult
|
||||
? { hresult: Number(identifierHresult) }
|
||||
: {}
|
||||
for (const match of output.matchAll(
|
||||
/\b(HResult|NativeErrorCode|ErrorCode)\s*[:=]\s*(0x[0-9a-f]{1,8}|-?\d{1,10})(?![\da-z])/giu
|
||||
)) {
|
||||
const key =
|
||||
match[1].toLowerCase() === 'hresult'
|
||||
? 'hresult'
|
||||
: match[1].toLowerCase() === 'nativeerrorcode'
|
||||
? 'nativeErrorCode'
|
||||
: 'errorCode'
|
||||
codes[key] = Number(match[2])
|
||||
}
|
||||
return {
|
||||
...(identifier && identifiers.some((known) => known === identifier)
|
||||
? { cimErrorId: identifier }
|
||||
: {}),
|
||||
...(category ? { powerShellCategory: category } : {}),
|
||||
...codes
|
||||
}
|
||||
}
|
||||
function classifyFailure(error: unknown): Record<string, unknown> {
|
||||
const message = error instanceof Error && error.message.length <= 262144 ? error.message : ''
|
||||
// execCommand includes the command before the output; never classify that script as an error.
|
||||
const commandFailure = /^Command "([\s\S]*)" failed \(exit (-?\d{1,10})\): ([\s\S]*)$/.exec(
|
||||
message
|
||||
)
|
||||
const output = diagnosticErrorText(
|
||||
(commandFailure?.[3] ?? (message.startsWith('Command "') ? '' : message)).slice(0, 16384)
|
||||
)
|
||||
let command = ''
|
||||
try {
|
||||
command = decodeRemotePowerShellScript(commandFailure?.[1] ?? '')
|
||||
} catch {
|
||||
/* Malformed diagnostics have no command phase. */
|
||||
}
|
||||
const wmiCreateFailure = /Win32_Process\.Create failed with (\d{1,10})/.exec(output)
|
||||
const commandPhase =
|
||||
command.includes('--spawn-detached') || command.includes('--detached')
|
||||
? 'detached-launch'
|
||||
: command.includes('--connect')
|
||||
? 'relay-connect'
|
||||
: command.includes('--version')
|
||||
? 'runtime-version'
|
||||
: commandFailure
|
||||
? 'remote-command'
|
||||
: undefined
|
||||
const categories = [
|
||||
['sftp', /sftp|subsystem/i],
|
||||
['permission', /permission|access(?: is)? denied|EACCES|EPERM/i],
|
||||
['missing-file', /not.found|ENOENT|missing/i],
|
||||
['timeout', /timed?.?out|timeout/i],
|
||||
['runtime', /bun|runtime/i],
|
||||
['integrity', /hash|sha256|integrity/i],
|
||||
['connection', /connection|channel|socket/i],
|
||||
['mock-contract', /not a function|mock/i],
|
||||
['command-not-found', /not recognized|command not found/i],
|
||||
['invalid-executable', /not a valid win32|bad exe|invalid image/i],
|
||||
['syntax', /syntax error|unexpected token|ParserError/i],
|
||||
['sharing-violation', /being used by another process|sharing violation/i],
|
||||
['cim', /CimException|Invoke-CimMethod|Win32_Process\.Create/i]
|
||||
] as const
|
||||
return {
|
||||
...structuredCimFailure(output),
|
||||
...(error &&
|
||||
typeof error === 'object' &&
|
||||
'code' in error &&
|
||||
typeof error.code === 'number' &&
|
||||
Number.isInteger(error.code)
|
||||
? { nativeExitCode: error.code }
|
||||
: {}),
|
||||
...(wmiCreateFailure ? { wmiCreateReturnCode: Number(wmiCreateFailure[1]) } : {}),
|
||||
...(commandFailure
|
||||
? {
|
||||
remoteExitCode: Number(commandFailure[2]),
|
||||
commandPhase,
|
||||
outputCharacters: output.length,
|
||||
outputLines: output
|
||||
.split(/\r?\n/)
|
||||
.slice(0, 12)
|
||||
.map((line, index) => ({
|
||||
index,
|
||||
categories: categories
|
||||
.filter(([, pattern]) => pattern.test(line))
|
||||
.map(([label]) => label)
|
||||
}))
|
||||
}
|
||||
: {}),
|
||||
kind:
|
||||
error instanceof Error
|
||||
? error.constructor.name.replace(/[^a-zA-Z0-9_]/g, '').slice(0, 64)
|
||||
: typeof error,
|
||||
categories: categories.filter(([, pattern]) => pattern.test(output)).map(([label]) => label),
|
||||
frames:
|
||||
error instanceof Error
|
||||
? [
|
||||
...(error.stack ?? '')
|
||||
.split('\n')
|
||||
.filter((line) => /^\s+at /u.test(line))
|
||||
.slice(0, 8)
|
||||
.join('\n')
|
||||
.slice(0, 16384)
|
||||
.matchAll(/([a-zA-Z0-9_-]+\.(?:ts|js|mjs|cjs)):(\d+):(\d+)/g)
|
||||
]
|
||||
.slice(0, 8)
|
||||
.map((match) => `${match[1]}:${match[2]}:${match[3]}`)
|
||||
: []
|
||||
}
|
||||
}
|
||||
it('retains numeric remote failure evidence without leaking command or output text', () => {
|
||||
const result = classifyFailure(
|
||||
new Error(
|
||||
'Command "bun.exe --detached secret-token permission-test" failed (exit 5): Access is denied.\nC:\\Users\\secret-user'
|
||||
)
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
remoteExitCode: 5,
|
||||
commandPhase: 'detached-launch',
|
||||
categories: ['permission'],
|
||||
outputLines: [
|
||||
{ index: 0, categories: ['permission'] },
|
||||
{ index: 1, categories: [] }
|
||||
]
|
||||
})
|
||||
expect(JSON.stringify(result)).not.toMatch(/secret|bun.exe|permission-test|Users/)
|
||||
const scriptOnly = classifyFailure(
|
||||
new Error('Command "bun.exe permission check" failed (exit 2): Nothing classified')
|
||||
)
|
||||
expect(scriptOnly).toMatchObject({ remoteExitCode: 2, categories: [] })
|
||||
})
|
||||
it('preserves the numeric WMI creation verdict', () => {
|
||||
expect(
|
||||
classifyFailure(
|
||||
new Error('Command "bun --detached" failed (exit 1): Win32_Process.Create failed with 2')
|
||||
)
|
||||
).toMatchObject({
|
||||
remoteExitCode: 1,
|
||||
wmiCreateReturnCode: 2,
|
||||
commandPhase: 'detached-launch',
|
||||
categories: ['cim']
|
||||
})
|
||||
})
|
||||
it('decodes compressed command phase without classifying its private command text', () => {
|
||||
const command = powerShellCommand(
|
||||
'bun.exe --detached secret-token permission-test; ' + '# private\n'.repeat(1000)
|
||||
)
|
||||
const result = classifyFailure(
|
||||
new Error(`Command "${command}" failed (exit 1): Nothing classified`)
|
||||
)
|
||||
expect(result).toMatchObject({ commandPhase: 'detached-launch', categories: [] })
|
||||
expect(JSON.stringify(result)).not.toMatch(/secret|private|bun.exe|permission-test/)
|
||||
})
|
||||
it('extracts only allowlisted CIM metadata from serialized PowerShell errors', () => {
|
||||
const output =
|
||||
'#< CLIXML\n<Objs><S S="Error">Invoke-CimMethod : Access is denied._x000D__x000A_' +
|
||||
'At line:1 char:2_x000D__x000A_+ bun.exe --detached secret-token_x000D__x000A_' +
|
||||
'+ CategoryInfo : PermissionDenied: (Win32_Process:String) [Invoke-CimMethod], CimException_x000D__x000A_' +
|
||||
'+ FullyQualifiedErrorId : HRESULT 0x80070005,Microsoft.Management.Infrastructure.CimCmdlets.InvokeCimMethodCommand_x000D__x000A_' +
|
||||
'HResult: -2147024891_x000D__x000A_NativeErrorCode: 5_x000D__x000A_' +
|
||||
'</S><S S="Verbose">bun.exe private secret-token</S></Objs>'
|
||||
const result = classifyFailure(new Error(`Command "private" failed (exit 1): ${output}`))
|
||||
expect(result).toMatchObject({
|
||||
categories: ['permission', 'cim'],
|
||||
cimErrorId: 'HRESULT 0x80070005',
|
||||
powerShellCategory: 'PermissionDenied',
|
||||
hresult: -2147024891,
|
||||
nativeErrorCode: 5
|
||||
})
|
||||
expect(result).not.toHaveProperty('wmiCreateReturnCode')
|
||||
expect(JSON.stringify(result)).not.toMatch(
|
||||
/secret|private|bun.exe|Win32_Process|InvokeCimMethodCommand/
|
||||
)
|
||||
})
|
||||
it('does not retain unknown identifiers, paths, numeric source echoes or incomplete XML', () => {
|
||||
const output =
|
||||
'+ secret-token HResult: 123\n' +
|
||||
'FullyQualifiedErrorId : C:\\Users\\secret-token,Microsoft.Management.Infrastructure.CimCmdlets.InvokeCimMethodCommand\n' +
|
||||
'CategoryInfo : secret-token: unknown\nHResult: 0x80041003\nNativeErrorCode: 12345678901234'
|
||||
const result = classifyFailure(new Error(output))
|
||||
expect(result).toMatchObject({ hresult: 2147749891 })
|
||||
expect(result).not.toHaveProperty('cimErrorId')
|
||||
expect(result).not.toHaveProperty('powerShellCategory')
|
||||
expect(result).not.toHaveProperty('nativeErrorCode')
|
||||
const unknownHresult = classifyFailure(
|
||||
new Error(
|
||||
'FullyQualifiedErrorId : HRESULT 0x80041001,Microsoft.Management.Infrastructure.CimCmdlets.InvokeCimMethodCommand'
|
||||
)
|
||||
)
|
||||
expect(unknownHresult).toMatchObject({ hresult: 2147749889 })
|
||||
expect(unknownHresult).not.toHaveProperty('cimErrorId')
|
||||
expect(JSON.stringify(result)).not.toMatch(/secret|Users/)
|
||||
expect(classifyFailure(new Error('#< CLIXML\n<S S="Error">permission'))).toMatchObject({
|
||||
categories: []
|
||||
})
|
||||
expect(
|
||||
classifyFailure(new Error('Command "private" failed (exit 1): ' + 'x'.repeat(262144)))
|
||||
).toMatchObject({ categories: [] })
|
||||
})
|
||||
const configPath = process.env.ORCA_SSH_PROBE_CONFIG
|
||||
it(
|
||||
'native ARM OpenSSH candidate provider preserves all settled rows and shell state',
|
||||
{ timeout: 600_000 },
|
||||
async () => {
|
||||
if (!configPath || !process.env.ORCA_SSH_PROBE_STATE)
|
||||
throw new Error('Explicit private SSH fixture configuration is required')
|
||||
expect(process.platform).toBe('win32')
|
||||
expect(process.arch).toBe('arm64')
|
||||
const config = record(JSON.parse(readFileSync(configPath!, 'utf8')))
|
||||
const source = textField(config, 'sourceCommit')
|
||||
expect(source).toMatch(/^[a-f0-9]{40}$/)
|
||||
// CI wrapper verifies git HEAD before starting this process and writes immutable receipt.
|
||||
expect(textField(config, 'observedSourceCommit')).toBe(source)
|
||||
const port = config.port
|
||||
if (typeof port !== 'number' || !Number.isInteger(port))
|
||||
throw new Error('Invalid private SSH port')
|
||||
const instance = `arm-native-${randomUUID()}`
|
||||
const createConnection = (): SshConnection =>
|
||||
new SshConnection(
|
||||
{
|
||||
id: instance,
|
||||
label: instance,
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
username: textField(config, 'username'),
|
||||
identityFile: textField(config, 'identityFile'),
|
||||
identitiesOnly: true,
|
||||
source: 'manual'
|
||||
},
|
||||
{
|
||||
onStateChange: () => {},
|
||||
onCredentialRequest: async () => {
|
||||
throw new Error('Interactive auth forbidden')
|
||||
}
|
||||
}
|
||||
)
|
||||
let conn = createConnection()
|
||||
const hello = { protocolVersion: 1, clientInstanceId: instance, requestedRole: 'session-owner' }
|
||||
let mux: SshChannelMultiplexer | undefined
|
||||
let grant: Record<string, unknown> | undefined
|
||||
const terminals = new Set<string>()
|
||||
let daemon: { pid: number; creationTimeMs?: number } | undefined
|
||||
const shellIdentities: { pid: number; creationTimeMs: number }[] = []
|
||||
let output = ''
|
||||
const fixtureDirectories: string[] = []
|
||||
let remoteRelayDirectory = ''
|
||||
let deployedRuntime = ''
|
||||
let candidate: ReturnType<typeof installCandidateOverride> | undefined
|
||||
let deploymentStarted = false
|
||||
let stage = 'connecting'
|
||||
const stages: string[] = []
|
||||
const receipts: Record<string, unknown> = { source, instance, cleanupVerified: false, stages }
|
||||
const deploy = async (): Promise<void> => {
|
||||
deploymentStarted = true
|
||||
stage = 'deployment'
|
||||
const allowedProgress = [
|
||||
'Detecting remote platform...',
|
||||
'Checking existing relay...',
|
||||
'Uploading relay...',
|
||||
'Installing native dependencies...',
|
||||
'Starting relay...'
|
||||
]
|
||||
const result = await deployAndLaunchRelay(
|
||||
conn,
|
||||
(status) => {
|
||||
if (allowedProgress.includes(status)) {
|
||||
stage = status
|
||||
if (stages.length < 32) stages.push(status)
|
||||
}
|
||||
},
|
||||
60,
|
||||
instance
|
||||
)
|
||||
stage = 'artifact-and-runtime-identity'
|
||||
expect(result.platform).toBe('win32-arm64')
|
||||
expect(result.nodePath?.toLowerCase()).toContain('bun')
|
||||
if (!result.remoteRelayDir) throw new Error('Missing actual remote relay directory')
|
||||
const artifactHashes: Record<string, string> = {}
|
||||
for (const filename of [
|
||||
'relay.js',
|
||||
'parcel-watcher.node',
|
||||
'windows-process-tree.node',
|
||||
...RELAY_WINDOWS_CONPTY_FILENAMES
|
||||
]) {
|
||||
const expected = createHash('sha256')
|
||||
.update(readFileSync(join(process.cwd(), 'out', 'relay', 'win32-arm64', filename)))
|
||||
.digest('hex')
|
||||
const actual = createHash('sha256')
|
||||
.update(readFileSync(join(result.remoteRelayDir, filename)))
|
||||
.digest('hex')
|
||||
expect(actual).toBe(expected)
|
||||
artifactHashes[filename] = actual
|
||||
}
|
||||
receipts.deployedArtifacts = artifactHashes
|
||||
const runtimePath = result.nodePath
|
||||
if (!runtimePath) throw new Error('Missing actual runtime executable')
|
||||
const runtimeHash = createHash('sha256').update(readFileSync(runtimePath)).digest('hex')
|
||||
expect(runtimeHash).toBe(ORCAD_BUN_RELEASE_ASSETS['win32-arm64'].executableSha256)
|
||||
remoteRelayDirectory = result.remoteRelayDir
|
||||
deployedRuntime = runtimePath
|
||||
receipts.runtime = {
|
||||
path: runtimePath,
|
||||
sha256: runtimeHash,
|
||||
expectedSourcePin: false,
|
||||
candidateOverride: true
|
||||
}
|
||||
if (!result.credentialFile) throw new Error('Deployment omitted private credential identity')
|
||||
const nextMux = new SshChannelMultiplexer(result.transport)
|
||||
mux = nextMux
|
||||
nextMux.onDispose(() => {
|
||||
if (mux === nextMux) mux = undefined
|
||||
})
|
||||
mux.onNotificationByMethod('pty.data', (message) => {
|
||||
if (typeof message.data === 'string') output = (output + message.data).slice(-65536)
|
||||
})
|
||||
const rows = await readWindowsProcessTableFresh()
|
||||
const normalizeCommand = (value: string): string => value.replaceAll('\\', '/').toLowerCase()
|
||||
const owners = rows.filter(
|
||||
(row) =>
|
||||
normalizeCommand(row.command).includes(normalizeCommand(result.credentialFile!)) &&
|
||||
row.command.includes('--detached')
|
||||
)
|
||||
expect(owners).toHaveLength(1)
|
||||
expect(owners[0].name.toLowerCase()).toBe('bun.exe')
|
||||
const command = normalizeCommand(owners[0].command)
|
||||
const executable = normalizeCommand(runtimePath)
|
||||
expect(
|
||||
command.startsWith('\"' + executable + '\" ') || command.startsWith(executable + ' ')
|
||||
).toBe(true)
|
||||
const descendants = new Set([owners[0].pid])
|
||||
for (let changed = true; changed;) {
|
||||
changed = false
|
||||
for (const row of rows)
|
||||
if (descendants.has(row.ppid) && !descendants.has(row.pid)) {
|
||||
descendants.add(row.pid)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
expect(
|
||||
rows.filter((row) => descendants.has(row.pid) && row.name.toLowerCase() === 'node.exe')
|
||||
).toEqual([])
|
||||
receipts.hostNodeExclusion = {
|
||||
relayImage: 'bun.exe',
|
||||
descendantSnapshotHasNode: false,
|
||||
scope: 'daemon and live descendants at deployment/reconnect snapshots'
|
||||
}
|
||||
if (!owners[0].creationTimeMs) throw new Error('Daemon process identity unverifiable')
|
||||
if (daemon) expect(owners[0]).toMatchObject(daemon)
|
||||
daemon = { pid: owners[0].pid, creationTimeMs: owners[0].creationTimeMs }
|
||||
receipts.daemon = daemon
|
||||
const admitted = record(
|
||||
await mux.request('pty.openClient', {
|
||||
...hello,
|
||||
...(grant
|
||||
? { resume: { ownerGeneration: grant.ownerGeneration, ownerLease: grant.ownerLease } }
|
||||
: {})
|
||||
})
|
||||
)
|
||||
if (grant) expect(admitted.resumed).toBe(true)
|
||||
if (!('ownerGeneration' in admitted) || typeof admitted.ownerLease !== 'string')
|
||||
throw new Error('Missing lease')
|
||||
grant = admitted
|
||||
}
|
||||
const spawn = async (): Promise<string> => {
|
||||
const result = record(
|
||||
await mux!.request('pty.spawn', { cols: 80, rows: 24, shellOverride: 'powershell.exe' })
|
||||
)
|
||||
const id = textField(result, 'id')
|
||||
terminals.add(id)
|
||||
return id
|
||||
}
|
||||
try {
|
||||
candidate = installCandidateOverride(config, process.env.ORCA_SSH_PROBE_STATE)
|
||||
receipts.candidateAdmission = candidate.receipt
|
||||
await conn.connect()
|
||||
await deploy()
|
||||
const id = await spawn()
|
||||
const nonce = randomUUID().replaceAll('-', '')
|
||||
output = ''
|
||||
mux!.notify('pty.data', {
|
||||
id,
|
||||
data: `$orcaProbe='${nonce}'; Write-Output ('READY_' + $orcaProbe + '_' + $PID)\r`
|
||||
})
|
||||
await expect.poll(() => output, { timeout: 30000 }).toContain(`READY_${nonce}_`)
|
||||
const before = output.match(new RegExp(`READY_${nonce}_(\\d+)`))?.[1]
|
||||
expect(before).toBeTruthy()
|
||||
const shell = (await readWindowsProcessTableFresh()).find((row) => row.pid === Number(before))
|
||||
if (!shell?.creationTimeMs) throw new Error('Shell identity unverifiable')
|
||||
shellIdentities.push({ pid: shell.pid, creationTimeMs: shell.creationTimeMs })
|
||||
mux!.dispose('connection_lost')
|
||||
mux = undefined
|
||||
await conn.disconnect()
|
||||
conn = createConnection()
|
||||
await conn.connect()
|
||||
await deploy()
|
||||
await mux!.request('pty.attach', { id })
|
||||
output = ''
|
||||
mux!.notify('pty.data', { id, data: "Write-Output ('AFTER_' + $orcaProbe + '_' + $PID)\r" })
|
||||
await expect.poll(() => output, { timeout: 30000 }).toContain(`AFTER_${nonce}_${before}`)
|
||||
receipts.sameShellState = true
|
||||
stage = 'provider-resize-and-loaded-images'
|
||||
receipts.repaint = await proveRepaint({
|
||||
mux: mux!,
|
||||
runtime: deployedRuntime,
|
||||
fixtureParent: dirname(remoteRelayDirectory),
|
||||
receiptPath: join(dirname(textField(config, 'receiptPath')), 'repaint-events.json'),
|
||||
ownTerminal: (terminalId) => {
|
||||
terminals.add(terminalId)
|
||||
},
|
||||
ownDirectory: (directory) => {
|
||||
fixtureDirectories.push(directory)
|
||||
},
|
||||
observeProvider: async () => {
|
||||
if (!daemon) throw new Error('Missing owned daemon identity')
|
||||
const evidence = await inspectProviderImages(daemon, remoteRelayDirectory)
|
||||
shellIdentities.push(...evidence.descendantIdentities)
|
||||
return evidence
|
||||
}
|
||||
})
|
||||
const fresh = await spawn()
|
||||
output = ''
|
||||
mux!.notify('pty.data', { id: fresh, data: "Write-Output ('FRESH_' + $PID)\r" })
|
||||
await expect.poll(() => output, { timeout: 30000 }).toMatch(/FRESH_\d+/)
|
||||
const freshPid = Number(output.match(/FRESH_(\d+)/)?.[1])
|
||||
const freshShell = (await readWindowsProcessTableFresh()).find((row) => row.pid === freshPid)
|
||||
if (!freshShell?.creationTimeMs) throw new Error('Fresh shell identity unverifiable')
|
||||
shellIdentities.push({ pid: freshShell.pid, creationTimeMs: freshShell.creationTimeMs })
|
||||
await mux!.request('pty.shutdown', { id: fresh, immediate: true })
|
||||
terminals.delete(fresh)
|
||||
} catch (error) {
|
||||
receipts.primaryFailure = { stage, ...classifyFailure(error) }
|
||||
throw new Error('Production SSH route failed; inspect sanitized primaryFailure receipt')
|
||||
} finally {
|
||||
try {
|
||||
if (!mux && daemon) {
|
||||
await conn.disconnect()
|
||||
conn = createConnection()
|
||||
await conn.connect()
|
||||
await deploy()
|
||||
}
|
||||
if (mux) {
|
||||
for (const id of terminals) await mux.request('pty.shutdown', { id, immediate: true })
|
||||
expect(await mux.request('pty.listProcesses', {})).toEqual([])
|
||||
mux.dispose()
|
||||
mux = undefined
|
||||
}
|
||||
await conn.disconnect()
|
||||
if (daemon)
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const rows = await readWindowsProcessTableFresh()
|
||||
return rows.some(
|
||||
(row) => row.pid === daemon!.pid && row.creationTimeMs === daemon!.creationTimeMs
|
||||
)
|
||||
},
|
||||
{ timeout: 90000, interval: 1000 }
|
||||
)
|
||||
.toBe(false)
|
||||
for (const shell of shellIdentities) {
|
||||
const rows = await readWindowsProcessTableFresh()
|
||||
expect(
|
||||
rows.some((row) => row.pid === shell.pid && row.creationTimeMs === shell.creationTimeMs)
|
||||
).toBe(false)
|
||||
}
|
||||
receipts.cleanupVerified = Boolean(daemon) || !deploymentStarted
|
||||
if (!receipts.cleanupVerified)
|
||||
throw new Error('Deployment may have launched an unobserved relay; cleanup unverifiable')
|
||||
for (const directory of fixtureDirectories)
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
receipts.cleanupFailure = classifyFailure(error)
|
||||
throw error
|
||||
} finally {
|
||||
candidate?.restore()
|
||||
receipts.sourcePinRestored = true
|
||||
mux?.dispose()
|
||||
try {
|
||||
await conn.disconnect()
|
||||
} finally {
|
||||
writeFileSync(textField(config, 'receiptPath'), JSON.stringify(receipts, null, 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { copyFileSync, mkdirSync, readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { ORCAD_BUN_RELEASE_ASSETS, ORCAD_BUN_VERSION } from '../../shared/orcad-bun-runtime'
|
||||
|
||||
export function installCandidateOverride(config: Record<string, unknown>, state: string) {
|
||||
const receiptPath = config.candidateReceiptPath
|
||||
const receiptHash = config.candidateReceiptSha256
|
||||
assert(
|
||||
typeof receiptPath === 'string' &&
|
||||
typeof receiptHash === 'string' &&
|
||||
/^[a-f0-9]{64}$/.test(receiptHash)
|
||||
)
|
||||
const bytes = readFileSync(receiptPath)
|
||||
assert.equal(
|
||||
createHash('sha256').update(bytes).digest('hex'),
|
||||
receiptHash,
|
||||
'preverified admission receipt changed'
|
||||
)
|
||||
const receipt = JSON.parse(bytes.toString('utf8'))
|
||||
assert.equal(receipt.producer, '34d1c11c67cbd1a653da1d07796daf7a1b0f2a7d')
|
||||
assert.equal(receipt.source, '744846f844374847c902b5e7fd59b4342a51ef99')
|
||||
assert.equal(receipt.patch, '276f475c90c6761c58b9f56b3f4bfafa079d0c29c5861b23d2320c9ff39c35fb')
|
||||
assert.equal(receipt.product, '2084c58ba5410106ce61153a9fb16cdb4b6e5301')
|
||||
assert.equal(String(receipt.producerRun), '36503596770')
|
||||
assert.equal(receipt.architecture, 'arm64')
|
||||
assert(typeof receipt.binary === 'string' && /^[a-f0-9]{64}$/.test(receipt.sha256))
|
||||
assert.equal(
|
||||
createHash('sha256').update(readFileSync(receipt.binary)).digest('hex'),
|
||||
receipt.sha256
|
||||
)
|
||||
const cache = join(state, 'orcad-artifacts', 'bun', `v${ORCAD_BUN_VERSION}`, 'win32-arm64')
|
||||
mkdirSync(cache, { recursive: true })
|
||||
copyFileSync(receipt.binary, join(cache, 'bun-runtime.exe'))
|
||||
const original = ORCAD_BUN_RELEASE_ASSETS['win32-arm64'].executableSha256
|
||||
ORCAD_BUN_RELEASE_ASSETS['win32-arm64'].executableSha256 = receipt.sha256
|
||||
return {
|
||||
receipt: {
|
||||
...receipt,
|
||||
receiptSha256: receiptHash,
|
||||
candidateOverride: true,
|
||||
originalExecutablePin: original,
|
||||
expectedSourcePin: false,
|
||||
scope: 'diagnostic process only; private cache; production deployment validation retained'
|
||||
},
|
||||
restore: () => {
|
||||
ORCAD_BUN_RELEASE_ASSETS['win32-arm64'].executableSha256 = original
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync, realpathSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { runProcess } from '../../shared/child-process/run-process'
|
||||
import { readWindowsProcessTableFresh } from '../windows/windows-process-table'
|
||||
import { WINDOWS_CONPTY_FILES } from '../../shared/windows-conpty-release'
|
||||
|
||||
export async function inspectProviderImages(
|
||||
daemon: { pid: number; creationTimeMs?: number },
|
||||
relayDirectory: string
|
||||
) {
|
||||
assert(Number.isInteger(daemon.pid) && daemon.pid > 0 && daemon.creationTimeMs)
|
||||
const before = await readWindowsProcessTableFresh()
|
||||
assert(
|
||||
before.some((row) => row.pid === daemon.pid && row.creationTimeMs === daemon.creationTimeMs)
|
||||
)
|
||||
const descendants = new Set([daemon.pid])
|
||||
for (let changed = true; changed;) {
|
||||
changed = false
|
||||
for (const row of before) {
|
||||
if (descendants.has(row.ppid) && !descendants.has(row.pid)) {
|
||||
descendants.add(row.pid)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
const consoles = before.filter(
|
||||
(row) => descendants.has(row.pid) && row.name.toLowerCase() === 'openconsole.exe'
|
||||
)
|
||||
assert(consoles.length > 0, 'no owned OpenConsole descendant')
|
||||
for (const row of consoles)
|
||||
assert(row.creationTimeMs, 'OpenConsole creation identity unavailable')
|
||||
// Scoped module/image query only; process enumeration uses the existing native table.
|
||||
const script = `$ErrorActionPreference='Stop'; $daemon=[Diagnostics.Process]::GetProcessById(${daemon.pid}); $modules=@($daemon.Modules | Where-Object {$_.ModuleName -ieq 'conpty.dll'} | ForEach-Object {$_.FileName}); $images=@(${consoles.map((row) => row.pid).join(',')} | ForEach-Object { $p=[Diagnostics.Process]::GetProcessById($_); @{pid=$p.Id;path=$p.MainModule.FileName;creationTimeMs=([DateTimeOffset]$p.StartTime.ToUniversalTime()).ToUnixTimeMilliseconds()} }); @{modules=$modules;images=$images} | ConvertTo-Json -Depth 4 -Compress`
|
||||
const result = await runProcess({
|
||||
program: join(
|
||||
process.env.SystemRoot ?? 'C:\\Windows',
|
||||
'System32',
|
||||
'WindowsPowerShell',
|
||||
'v1.0',
|
||||
'powershell.exe'
|
||||
),
|
||||
args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script],
|
||||
timeoutMs: 15000,
|
||||
env: { ORCA_BACKGROUND_LAUNCH: '1' },
|
||||
maxOutputBytes: 65536
|
||||
})
|
||||
assert(!result.timedOut && result.code === 0, 'scoped provider module inspection failed')
|
||||
const evidence = JSON.parse(result.stdout)
|
||||
assert(Array.isArray(evidence.modules) && evidence.modules.length === 1)
|
||||
const samePath = (a: string, b: string) =>
|
||||
realpathSync(a).toLowerCase() === realpathSync(b).toLowerCase()
|
||||
assert(
|
||||
samePath(evidence.modules[0], join(relayDirectory, 'conpty.dll')),
|
||||
'relay loaded foreign ConPTY'
|
||||
)
|
||||
assert(Array.isArray(evidence.images) && evidence.images.length === consoles.length)
|
||||
for (const image of evidence.images) {
|
||||
const original = consoles.find((row) => row.pid === image.pid)
|
||||
assert(
|
||||
original?.creationTimeMs && Math.abs(original.creationTimeMs - image.creationTimeMs) <= 1,
|
||||
'console identity changed during query'
|
||||
)
|
||||
assert(
|
||||
samePath(image.path, join(relayDirectory, 'OpenConsole.exe')),
|
||||
'foreign OpenConsole image'
|
||||
)
|
||||
assert.equal(
|
||||
createHash('sha256').update(readFileSync(image.path)).digest('hex'),
|
||||
WINDOWS_CONPTY_FILES.arm64['OpenConsole.exe']
|
||||
)
|
||||
}
|
||||
assert.equal(
|
||||
createHash('sha256').update(readFileSync(evidence.modules[0])).digest('hex'),
|
||||
WINDOWS_CONPTY_FILES.arm64['conpty.dll']
|
||||
)
|
||||
const after = await readWindowsProcessTableFresh()
|
||||
for (const original of [daemon, ...consoles]) {
|
||||
assert(
|
||||
after.some(
|
||||
(row) => row.pid === original.pid && row.creationTimeMs === original.creationTimeMs
|
||||
),
|
||||
'owner changed during module observation'
|
||||
)
|
||||
}
|
||||
const descendantIdentities = before
|
||||
.filter((row) => descendants.has(row.pid) && row.pid !== daemon.pid)
|
||||
.map((row) => {
|
||||
assert(typeof row.creationTimeMs === 'number', 'descendant cleanup identity unavailable')
|
||||
return { pid: row.pid, creationTimeMs: row.creationTimeMs }
|
||||
})
|
||||
return { ...evidence, providerHashes: WINDOWS_CONPTY_FILES.arm64, daemon, descendantIdentities }
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { setTimeout as delay } from 'node:timers/promises'
|
||||
import { HeadlessEmulator } from '../daemon/headless-emulator'
|
||||
import { readWrappedLineGlyphs } from '../daemon/__fixtures__/terminal-wide-cell-grid'
|
||||
import type { SshChannelMultiplexer } from './ssh-channel-multiplexer'
|
||||
import { powerShellLiteral } from './ssh-remote-powershell'
|
||||
|
||||
// Exact fixture and oracle from pty-repaint-wide-char-buffer.bun.test.ts.
|
||||
const korean = '안녕하세요 오르카 테스트입니다. 결론부터 말씀드리면 시각적 피로도'
|
||||
const latin = 'roadmap/complete-overhaul-backlog-history.md (1.75) R-08)'
|
||||
type Event = { data: string } | { resizeTo: number }
|
||||
class ProbeEmulator extends HeadlessEmulator {
|
||||
lines(): string[] {
|
||||
return readWrappedLineGlyphs(this.terminal).filter((line) => line.length > 0)
|
||||
}
|
||||
}
|
||||
function assertEight(events: Event[]): string[] {
|
||||
const emulator = new ProbeEmulator({ cols: 40, rows: 12 })
|
||||
try {
|
||||
for (const event of events) {
|
||||
if ('resizeTo' in event) emulator.resize(event.resizeTo, 12)
|
||||
else emulator.writeSync(event.data)
|
||||
}
|
||||
const lines = emulator.lines()
|
||||
const ko = korean.replace(/\s+/g, '')
|
||||
const la = latin.replace(/\s+/g, '')
|
||||
assert.equal(
|
||||
lines.filter((line) => line === ko).length,
|
||||
8,
|
||||
'all eight Korean rows must survive'
|
||||
)
|
||||
assert.equal(lines.filter((line) => line === la).length, 8, 'all eight Latin rows must survive')
|
||||
assert.deepEqual(
|
||||
lines.filter((line) => line !== ko && line !== la),
|
||||
[],
|
||||
'unexpected fixture rows'
|
||||
)
|
||||
return lines
|
||||
} finally {
|
||||
emulator.dispose()
|
||||
}
|
||||
}
|
||||
export async function proveRepaint(options: {
|
||||
mux: SshChannelMultiplexer
|
||||
runtime: string
|
||||
fixtureParent: string
|
||||
receiptPath: string
|
||||
ownTerminal(id: string): void
|
||||
ownDirectory(directory: string): void
|
||||
observeProvider(): Promise<unknown>
|
||||
}): Promise<Record<string, unknown>> {
|
||||
const directory = join(options.fixtureParent, `provider-probe-${randomUUID()}`)
|
||||
mkdirSync(directory)
|
||||
options.ownDirectory(directory)
|
||||
const script = join(directory, 'repaint.cjs')
|
||||
const settled = join(directory, 'settled')
|
||||
const dimensions = join(directory, 'dimensions')
|
||||
const source = `const fs=require('node:fs');process.stdout.write('\\x1bc');let i=0;const timer=setInterval(()=>{process.stdout.write(${JSON.stringify(korean)}+'\\r\\n'+${JSON.stringify(latin)}+'\\r\\n');if(++i===8){clearInterval(timer);process.stdout.write('',()=>fs.writeFileSync(${JSON.stringify(settled)},''));}},25);setInterval(()=>fs.writeFileSync(${JSON.stringify(dimensions)},String(process.stdout.columns)),20);setTimeout(()=>process.exit(0),45000);`
|
||||
writeFileSync(script, source)
|
||||
const events: Event[] = []
|
||||
let id = ''
|
||||
let lastOutput = performance.now()
|
||||
let overflow = false
|
||||
let bytes = 0
|
||||
const subscription = options.mux.onNotificationByMethod('pty.data', (message) => {
|
||||
if (message.id === id && typeof message.data === 'string') {
|
||||
bytes += Buffer.byteLength(message.data)
|
||||
if (events.length > 10000 || bytes > 1048576) {
|
||||
overflow = true
|
||||
return
|
||||
}
|
||||
events.push({ data: message.data })
|
||||
lastOutput = performance.now()
|
||||
}
|
||||
})
|
||||
const quiet = async (cols: number) => {
|
||||
const deadline = performance.now() + 15000
|
||||
while (performance.now() < deadline) {
|
||||
assert(!overflow, 'Repaint event budget exceeded')
|
||||
if (
|
||||
existsSync(settled) &&
|
||||
existsSync(dimensions) &&
|
||||
readFileSync(dimensions, 'utf8') === String(cols) &&
|
||||
performance.now() - lastOutput > 250
|
||||
)
|
||||
return
|
||||
await delay(25)
|
||||
}
|
||||
throw new Error('Fixture did not settle')
|
||||
}
|
||||
try {
|
||||
const spawned: unknown = await options.mux.request('pty.spawn', {
|
||||
cols: 40,
|
||||
rows: 12,
|
||||
shellOverride: 'powershell.exe'
|
||||
})
|
||||
assert(
|
||||
spawned && typeof spawned === 'object' && 'id' in spawned && typeof spawned.id === 'string'
|
||||
)
|
||||
id = spawned.id
|
||||
options.ownTerminal(id)
|
||||
options.mux.notify('pty.data', {
|
||||
id,
|
||||
data: `& ${powerShellLiteral(options.runtime)} --no-env-file --config=NUL --no-install ${powerShellLiteral(script)}\r`
|
||||
})
|
||||
await quiet(40)
|
||||
const initial = assertEight(events)
|
||||
const provider = await options.observeProvider()
|
||||
const settledWidths: { cols: number; lines: string[] }[] = [{ cols: 40, lines: initial }]
|
||||
for (const cols of [31, 47]) {
|
||||
events.push({ resizeTo: cols })
|
||||
options.mux.notify('pty.resize', { id, cols, rows: 12 })
|
||||
// Give the remote resize time to arrive before measuring output quiescence.
|
||||
await delay(350)
|
||||
await quiet(cols)
|
||||
settledWidths.push({ cols, lines: assertEight(events) })
|
||||
}
|
||||
const final = assertEight(events)
|
||||
return {
|
||||
initial,
|
||||
final,
|
||||
resizeOrder: [40, 31, 47],
|
||||
settledWidths,
|
||||
provider,
|
||||
fixtureDirectory: directory,
|
||||
koreanRows: 8,
|
||||
latinRows: 8,
|
||||
exactRowsOnly: true
|
||||
}
|
||||
} finally {
|
||||
subscription()
|
||||
writeFileSync(
|
||||
options.receiptPath,
|
||||
JSON.stringify({ directory, terminalId: id, events }, null, 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user