[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ConfigPath,
[string]$ServiceAddress,
[string]$WebAddress,
[string]$ProviderBaseUrl,
[switch]$ProbeListeners,
[switch]$RequireTokenFile
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Get-EndpointParts([string]$Address) {
if ([string]::IsNullOrWhiteSpace($Address)) {
return $null
}
if ($Address -match '^\[(?<host>[^\]]+)\]:(?<port>\d+)$') {
$hostName = $Matches.host
$port = [int]$Matches.port
} elseif ($Address -match '^(?<host>[^:]+):(?<port>\d+)$') {
$hostName = $Matches.host
$port = [int]$Matches.port
} else {
return $null
}
if ($port -lt 1 -or $port -gt 65535) {
return $null
}
[pscustomobject]@{ Host = $hostName; Port = $port }
}
if (-not (Test-Path -LiteralPath $ConfigPath -PathType Leaf)) {
[pscustomobject]@{
result = 'HANDOFF'
reason = 'production configuration file was not provided'
config_path = [IO.Path]::GetFullPath($ConfigPath)
secrets_loaded = $false
} | ConvertTo-Json -Depth 4
exit 2
}
$required = @(
'CODEXMANAGER_STORAGE_BACKEND',
'CODEXMANAGER_DATABASE_URL',
'CODEXMANAGER_UPSTREAM_BASE_URL',
'CODEXMANAGER_SERVICE_ADDR',
'CODEXMANAGER_WEB_ADDR',
'CODEXMANAGER_LOGIN_ADDR',
'CODEXMANAGER_RPC_TOKEN_FILE'
)
$present = @{}
$values = @{}
foreach ($line in Get-Content -LiteralPath $ConfigPath) {
if ($line -match '^\s*(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?<value>.*)$' -and
-not [string]::IsNullOrWhiteSpace($Matches.value.Trim().Trim('"', "'"))) {
$present[$Matches.name] = $true
$values[$Matches.name] = $Matches.value.Trim().Trim('"', "'")
}
}
$missing = @($required | Where-Object { -not $present.ContainsKey($_) })
$placeholderPattern = '(?i)(REPLACE_ME|CHANGE_ME|TODO|YOUR_[A-Z0-9_]+|<[^>]+>)'
$placeholders = @($required | Where-Object {
$present.ContainsKey($_) -and $values[$_] -match $placeholderPattern
})
$semanticErrors = @()
if ($present.ContainsKey('CODEXMANAGER_STORAGE_BACKEND') -and
$values['CODEXMANAGER_STORAGE_BACKEND'].ToLowerInvariant() -ne 'mysql') {
$semanticErrors += 'CODEXMANAGER_STORAGE_BACKEND must be mysql for controlled production deployment'
}
if ($present.ContainsKey('CODEXMANAGER_DATABASE_URL') -and
$values['CODEXMANAGER_DATABASE_URL'] -notmatch '(?i)^mysql(?:\+[^:]+)?://') {
$semanticErrors += 'CODEXMANAGER_DATABASE_URL must use a mysql:// URL for controlled production deployment'
}
foreach ($addressName in @('CODEXMANAGER_SERVICE_ADDR', 'CODEXMANAGER_WEB_ADDR', 'CODEXMANAGER_LOGIN_ADDR')) {
if ($present.ContainsKey($addressName) -and $null -eq (Get-EndpointParts $values[$addressName])) {
$semanticErrors += "$addressName must use host:port syntax"
}
}
$tokenFileResult = $null
if ($RequireTokenFile -and $present.ContainsKey('CODEXMANAGER_RPC_TOKEN_FILE')) {
$tokenPath = [Environment]::ExpandEnvironmentVariables($values['CODEXMANAGER_RPC_TOKEN_FILE'])
$tokenFileResult = [pscustomobject]@{
path_supplied = $true
exists = [bool](Test-Path -LiteralPath $tokenPath -PathType Leaf)
non_empty = if (Test-Path -LiteralPath $tokenPath -PathType Leaf) {
([IO.FileInfo]$tokenPath).Length -gt 0
} else { $false }
}
}
$listenerResults = @()
if ($ProbeListeners) {
foreach ($address in @($ServiceAddress, $WebAddress)) {
$parts = Get-EndpointParts $address
if ($null -eq $parts) {
$listenerResults += [pscustomobject]@{ address = $address; reachable = $false; reason = 'address not supplied as host:port' }
continue
}
$ok = Test-NetConnection -ComputerName $parts.Host -Port $parts.Port -InformationLevel Quiet -WarningAction SilentlyContinue
$listenerResults += [pscustomobject]@{ address = $address; reachable = [bool]$ok }
}
}
$tokenFileMissing = $RequireTokenFile -and ($null -eq $tokenFileResult -or -not $tokenFileResult.exists -or -not $tokenFileResult.non_empty)
$unreachableListener = $ProbeListeners -and @($listenerResults | Where-Object { -not $_.reachable }).Count -gt 0
$result = if ($missing.Count -eq 0 -and $placeholders.Count -eq 0 -and $semanticErrors.Count -eq 0 -and -not $tokenFileMissing -and -not $unreachableListener) {
'READY_FOR_AUTHENTICATED_ACCEPTANCE'
} else { 'HANDOFF' }
[pscustomobject]@{
result = $result
config_path = [IO.Path]::GetFullPath($ConfigPath)
secrets_loaded = $false
required_variables_present = ($missing.Count -eq 0)
missing_variables = $missing
placeholder_variables = $placeholders
semantic_errors = $semanticErrors
token_file = $tokenFileResult
listeners = $listenerResults
provider_probe = if ([string]::IsNullOrWhiteSpace($ProviderBaseUrl)) {
'not_run: requires explicit production credentials and acceptance scope'
} else {
'not_run: provider URL supplied as an explicit probe input; no request was sent'
}
note = 'Values, credentials, and provider responses are intentionally omitted from this report.'
} | ConvertTo-Json -Depth 5
if ($missing.Count -ne 0 -or $placeholders.Count -ne 0 -or $semanticErrors.Count -ne 0 -or $tokenFileMissing -or $unreachableListener) { exit 2 }