1420 lines
68 KiB
PowerShell
1420 lines
68 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$EvidenceRoot,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$PackageFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$RuntimeConfigurationFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$CustomerProfileFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$RolloutPolicyFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$CommercialPreflightFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$PurchaseReadOnlySessionPreflightFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$PurchaseFieldReadOnlyValidationHandoffFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$LeaveReadOnlySessionPreflightFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$LeaveFieldReadOnlyValidationHandoffFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$DiagnosisReadOnlySessionPreflightFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$DiagnosisFieldReadOnlyValidationHandoffFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$MiniMaxVisionProbeFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$LegacyBuildEvidenceFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$PurchaseWorkflowUatAuthorizationFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$LeaveWorkflowUatAuthorizationFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$PurchaseAcceptanceFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$PurchaseWriteIntegrationFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$LeaveAcceptanceFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$LeaveWriteIntegrationFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$DiagnosticMissingFieldFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$DiagnosticPermissionDeniedFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$DiagnosticInvalidRelationFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$DiagnosticInitializationSqlFailureFileName,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$')][string]$OutputFileName,
|
|
[Parameter(Mandatory = $true)][string]$VerifierCliPath,
|
|
[Parameter(Mandatory = $true)][string]$RuntimeCliPath,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}$')][string]$ExpectedRuntimeCliVersion,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Fa-f0-9]{40}$')][string]$ExpectedSourceCommit,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Fa-f0-9]{64}$')][string]$ExpectedPackageSha256,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$')][string]$RolloutCustomerId,
|
|
[Parameter(Mandatory = $true)][string]$AccountBook,
|
|
[Parameter(Mandatory = $true)][string]$PurchaseSubSystemId,
|
|
[Parameter(Mandatory = $true)][string]$LeaveSubSystemId,
|
|
[Parameter(Mandatory = $true)][string]$DiagnosisSubSystemId,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Fa-f0-9]{64}$')][string]$ExpectedDatabaseScopeFingerprint,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_.:-]{1,64}$')][string]$PurchaseModuleCode,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_.:-]{1,64}$')][string]$LeaveModuleCode,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_.:-]{1,128}$')][string]$ValidatedBy,
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Fa-f0-9 ]{40,59}$')][string]$CertificateThumbprint,
|
|
[ValidateRange(1, 366)][int]$ValidDays = 90
|
|
)
|
|
|
|
Set-StrictMode -Version 2.0
|
|
$ErrorActionPreference = 'Stop'
|
|
$requiredPreflightChecks = @(
|
|
'windows_os', 'package_hashes', 'package_source_provenance',
|
|
'package_archive_binding',
|
|
'sqlserver_compatibility100_syntax',
|
|
'legacy_build_tool', 'legacy_build_contract_tests',
|
|
'workflow_write_evidence_tools', 'workflow_uat_case_catalog',
|
|
'legacy_build_evidence',
|
|
'legacy_erp_authenticode', 'legacy_cli_authenticode',
|
|
'legacy_bridge_authenticode', 'desktop_host', 'host_authenticode',
|
|
'bridge_cli_runtime_identity',
|
|
'webview2_runtime', 'guga_supply_chain_audit', 'guga_sprite',
|
|
'guga_commercial_license_evidence',
|
|
'astrbot_agpl_eula_compliance_evidence',
|
|
'minimax_api_service_compliance_evidence',
|
|
'minimax_online_vision_probe_evidence',
|
|
'astrbot_credential', 'desktop_secret_environment', 'astrbot_transport',
|
|
'astrbot_reachable', 'astrbot_runtime_contract',
|
|
'rollout_policy_file', 'erp_agent_bridge', 'erp_rollout_policy',
|
|
'erp_session_scope',
|
|
'astrbot_plugin_layout', 'astrbot_plugin_contract',
|
|
'minimax_direct_https_vlm', 'pdf_invoice_pipeline',
|
|
'attachment_snapshot_binding',
|
|
'erp_wire_contract'
|
|
)
|
|
|
|
function Get-Sha256Hex([byte[]]$Bytes) {
|
|
$sha = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
return ([BitConverter]::ToString($sha.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant()
|
|
}
|
|
finally { $sha.Dispose() }
|
|
}
|
|
|
|
function Get-ReadOnlySessionScopeHash([string]$Scope, [string]$Value) {
|
|
$bytes = [Text.UTF8Encoding]::new($false, $true).GetBytes(
|
|
'lserp-readonly-preflight-v1|' + $Scope + '|' + $Value)
|
|
return Get-Sha256Hex $bytes
|
|
}
|
|
|
|
function Get-Artifact([string]$Name, [string]$FileName, [long]$MaximumBytes) {
|
|
if ([IO.Path]::GetFileName($FileName) -ne $FileName -or $FileName -in @('.', '..')) {
|
|
throw "$Name filename must be a plain basename."
|
|
}
|
|
$path = Join-Path $script:evidenceRootPath $FileName
|
|
if (-not [IO.File]::Exists($path)) { throw "$Name file does not exist." }
|
|
$item = Get-Item -LiteralPath $path -Force
|
|
if ($item.Length -le 0 -or $item.Length -gt $MaximumBytes -or
|
|
(($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)) {
|
|
throw "$Name must be a non-empty ordinary file within the size limit."
|
|
}
|
|
$stream = [IO.File]::Open($path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)
|
|
try {
|
|
$sha = [Security.Cryptography.SHA256]::Create()
|
|
try { $hash = ([BitConverter]::ToString($sha.ComputeHash($stream))).Replace('-', '').ToLowerInvariant() }
|
|
finally { $sha.Dispose() }
|
|
}
|
|
finally { $stream.Dispose() }
|
|
return [ordered]@{
|
|
path = $path
|
|
fileName = $FileName
|
|
sizeBytes = [long]$item.Length
|
|
sha256 = $hash
|
|
}
|
|
}
|
|
|
|
function Test-ExactProperties([object]$Value, [string[]]$Expected) {
|
|
if ($null -eq $Value) { return $false }
|
|
$names = @($Value.PSObject.Properties | ForEach-Object { $_.Name })
|
|
if ($names.Count -ne $Expected.Count) { return $false }
|
|
foreach ($name in $Expected) { if ($names -cnotcontains $name) { return $false } }
|
|
return $true
|
|
}
|
|
|
|
function Test-JsonArray([object]$Value) {
|
|
return $null -ne $Value -and $Value -is [array]
|
|
}
|
|
|
|
function Get-PackagedRuntimeCliContract(
|
|
[string]$PackagePath,
|
|
[Text.Encoding]$StrictUtf8) {
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
$archive = [IO.Compression.ZipFile]::OpenRead($PackagePath)
|
|
try {
|
|
$manifestEntries = @($archive.Entries | Where-Object {
|
|
[string]$_.FullName -cmatch '(?:^|/)SHA256SUMS\.json$'
|
|
})
|
|
if ($manifestEntries.Count -ne 1 -or
|
|
$manifestEntries[0].Length -le 0 -or
|
|
$manifestEntries[0].Length -gt 4MB) {
|
|
throw 'Package must contain one bounded SHA256SUMS.json.'
|
|
}
|
|
$stream = $manifestEntries[0].Open()
|
|
$reader = $null
|
|
try {
|
|
$reader = [IO.StreamReader]::new(
|
|
$stream, $StrictUtf8, $false, 4096, $false)
|
|
$manifestText = $reader.ReadToEnd()
|
|
}
|
|
finally {
|
|
if ($null -ne $reader) { $reader.Dispose() }
|
|
else { $stream.Dispose() }
|
|
}
|
|
try { $manifest = $manifestText | ConvertFrom-Json }
|
|
catch { throw 'Package SHA256SUMS.json is invalid JSON.' }
|
|
if (-not (Test-ExactProperties $manifest @(
|
|
'schemaVersion', 'packageVersion', 'generatedAtUtc',
|
|
'files')) -or
|
|
[string]$manifest.schemaVersion -cne '1.0' -or
|
|
[string]$manifest.packageVersion -cne
|
|
$ExpectedRuntimeCliVersion -or
|
|
-not (Test-JsonArray $manifest.files)) {
|
|
throw 'Package manifest version contract is invalid.'
|
|
}
|
|
$entries = @($manifest.files | Where-Object {
|
|
[string]$_.path -ceq 'Host/lserp-agent-cli.exe'
|
|
})
|
|
if ($entries.Count -ne 1 -or
|
|
-not (Test-ExactProperties $entries[0] @(
|
|
'path', 'sizeBytes', 'sha256')) -or
|
|
[long]$entries[0].sizeBytes -le 0 -or
|
|
[long]$entries[0].sizeBytes -gt 128MB -or
|
|
[string]$entries[0].sha256 -cnotmatch '^[a-f0-9]{64}$') {
|
|
throw 'Package runtime CLI manifest entry is invalid.'
|
|
}
|
|
return [pscustomobject]@{
|
|
Version = [string]$manifest.packageVersion
|
|
SizeBytes = [long]$entries[0].sizeBytes
|
|
Sha256 = [string]$entries[0].sha256
|
|
}
|
|
}
|
|
finally { $archive.Dispose() }
|
|
}
|
|
|
|
function Read-StrictJsonInt32(
|
|
[object]$Raw,
|
|
[int]$Minimum,
|
|
[int]$Maximum,
|
|
[string]$Message
|
|
) {
|
|
if (($Raw -isnot [int] -and $Raw -isnot [long]) -or
|
|
[long]$Raw -lt $Minimum -or [long]$Raw -gt $Maximum) {
|
|
throw $Message
|
|
}
|
|
return [int]$Raw
|
|
}
|
|
|
|
function Assert-ReadOnlyNativeExecutionProfile(
|
|
[object]$Profile,
|
|
[string]$Action,
|
|
[string]$ModuleKind
|
|
) {
|
|
$message = 'ERP read-only native execution profile is invalid.'
|
|
if (-not (Test-ExactProperties $Profile @(
|
|
'available', 'nativeSaveFamily',
|
|
'profileFingerprint', 'code')) -or
|
|
$Profile.available -isnot [bool]) {
|
|
throw $message
|
|
}
|
|
if ([bool]$Profile.available) {
|
|
$allowedFamilies = if ($ModuleKind -ceq 'document') {
|
|
@(
|
|
'legacy.bill-save.p-bill-save-pr3',
|
|
'legacy.bill-save.p-bill-save-pr70'
|
|
)
|
|
} else {
|
|
@(
|
|
'legacy.base-save.p-base-save',
|
|
'legacy.base-save.p-base-save70'
|
|
)
|
|
}
|
|
if ($Profile.nativeSaveFamily -isnot [string] -or
|
|
[string]$Profile.nativeSaveFamily -cnotin $allowedFamilies -or
|
|
$Profile.profileFingerprint -isnot [string] -or
|
|
[string]$Profile.profileFingerprint -cnotmatch
|
|
'^[a-f0-9]{64}$' -or
|
|
$null -ne $Profile.code -or
|
|
($Action -ceq 'update' -and $ModuleKind -ceq 'document')) {
|
|
throw $message
|
|
}
|
|
}
|
|
else {
|
|
if ($null -ne $Profile.nativeSaveFamily -or
|
|
$null -ne $Profile.profileFingerprint -or
|
|
$Profile.code -isnot [string] -or
|
|
[string]$Profile.code -cnotmatch '^[a-z0-9_.-]{1,128}$' -or
|
|
($Action -ceq 'update' -and $ModuleKind -ceq 'document' -and
|
|
[string]$Profile.code -cne
|
|
'dynamic_module_update_bill_unsupported')) {
|
|
throw $message
|
|
}
|
|
}
|
|
}
|
|
|
|
function Assert-ReadOnlyPayloadContract(
|
|
[object]$Payload,
|
|
[string]$ModuleKind,
|
|
[int]$MasterCount,
|
|
[int]$DetailCount,
|
|
[int]$EditableCount,
|
|
[int]$RequiredCount
|
|
) {
|
|
$message = 'ERP read-only parameter payload evidence is invalid.'
|
|
if (-not (Test-ExactProperties $Payload @(
|
|
'format', 'valueEncoding', 'moduleCodeRequired',
|
|
'masterValuesRequired', 'detailRowsRequired',
|
|
'minimumDetailRows', 'maximumDetailRows',
|
|
'masterParameterIdCount',
|
|
'requiredMasterParameterIdCount',
|
|
'detailParameterIdCount',
|
|
'requiredDetailParameterIdCount',
|
|
'unknownParameterPolicy', 'duplicateParameterPolicy',
|
|
'lookupPolicy', 'fieldConstraintPolicy',
|
|
'configurationDriftPolicy')) -or
|
|
$Payload.format -isnot [string] -or
|
|
[string]$Payload.format -cne 'parameter_entries_v1' -or
|
|
$Payload.valueEncoding -isnot [string] -or
|
|
[string]$Payload.valueEncoding -cne 'invariant_text' -or
|
|
$Payload.moduleCodeRequired -isnot [bool] -or
|
|
$Payload.moduleCodeRequired -ne $true -or
|
|
$Payload.masterValuesRequired -isnot [bool] -or
|
|
$Payload.masterValuesRequired -ne $true -or
|
|
$Payload.detailRowsRequired -isnot [bool] -or
|
|
$Payload.unknownParameterPolicy -isnot [string] -or
|
|
[string]$Payload.unknownParameterPolicy -cne 'reject' -or
|
|
$Payload.duplicateParameterPolicy -isnot [string] -or
|
|
[string]$Payload.duplicateParameterPolicy -cne 'reject' -or
|
|
$Payload.lookupPolicy -isnot [string] -or
|
|
[string]$Payload.lookupPolicy -cne
|
|
'server_resolve_unique_or_stop' -or
|
|
$Payload.fieldConstraintPolicy -isnot [string] -or
|
|
[string]$Payload.fieldConstraintPolicy -cne
|
|
'server_enforced_from_current_low_code_configuration' -or
|
|
$Payload.configurationDriftPolicy -isnot [string] -or
|
|
[string]$Payload.configurationDriftPolicy -cne
|
|
'reject_and_replan') {
|
|
throw $message
|
|
}
|
|
|
|
$minimumRows = Read-StrictJsonInt32 `
|
|
$Payload.minimumDetailRows 0 1 $message
|
|
$maximumRows = Read-StrictJsonInt32 `
|
|
$Payload.maximumDetailRows 0 1000 $message
|
|
$masterIds = Read-StrictJsonInt32 `
|
|
$Payload.masterParameterIdCount 0 $MasterCount $message
|
|
$requiredMasterIds = Read-StrictJsonInt32 `
|
|
$Payload.requiredMasterParameterIdCount 0 $MasterCount $message
|
|
$detailIds = Read-StrictJsonInt32 `
|
|
$Payload.detailParameterIdCount 0 $DetailCount $message
|
|
$requiredDetailIds = Read-StrictJsonInt32 `
|
|
$Payload.requiredDetailParameterIdCount 0 $DetailCount $message
|
|
$document = $ModuleKind -ceq 'document'
|
|
if ([bool]$Payload.detailRowsRequired -ne $document -or
|
|
$minimumRows -ne $(if ($document) { 1 } else { 0 }) -or
|
|
$maximumRows -ne $(if ($document) { 1000 } else { 0 }) -or
|
|
$requiredMasterIds -gt $masterIds -or
|
|
$requiredDetailIds -gt $detailIds -or
|
|
$masterIds + $detailIds -ne $EditableCount -or
|
|
$requiredMasterIds + $requiredDetailIds -ne $RequiredCount -or
|
|
(-not $document -and
|
|
($detailIds -ne 0 -or $requiredDetailIds -ne 0))) {
|
|
throw $message
|
|
}
|
|
}
|
|
|
|
function Assert-ReadOnlyModuleEvidence([object]$Module) {
|
|
$message = 'ERP read-only dynamic module evidence is invalid.'
|
|
if (-not (Test-ExactProperties $Module @(
|
|
'requestedCode', 'moduleCode', 'moduleKind',
|
|
'contractSource', 'sessionScopeBound',
|
|
'configurationFingerprint', 'contractFingerprint',
|
|
'masterParameterCount', 'detailParameterCount',
|
|
'inputModeCounts', 'editableParameterCount',
|
|
'requiredParameterCount',
|
|
'dedicatedAdapterParameterCount',
|
|
'nativeExecutionProfiles', 'payloadContract',
|
|
'genericWriteExecutionAvailable',
|
|
'writeExecutionBlocked', 'writeCommand',
|
|
'writeReadinessCode',
|
|
'genericUpdateExecutionAvailable',
|
|
'updateExecutionBlocked', 'updateCommand',
|
|
'updateReadinessCode', 'planExecutionAllowed')) -or
|
|
$Module.requestedCode -isnot [string] -or
|
|
[string]$Module.requestedCode -cnotmatch
|
|
'^[A-Za-z0-9_.:-]{1,64}$' -or
|
|
$Module.moduleCode -isnot [string] -or
|
|
[string]$Module.moduleCode -cnotmatch
|
|
'^[A-Za-z0-9_.:-]{1,64}$' -or
|
|
$Module.moduleKind -isnot [string] -or
|
|
[string]$Module.moduleKind -cnotin @('document', 'master_data') -or
|
|
$Module.contractSource -isnot [string] -or
|
|
[string]$Module.contractSource -cne
|
|
'current_erp_database_low_code_configuration' -or
|
|
$Module.sessionScopeBound -isnot [bool] -or
|
|
$Module.sessionScopeBound -ne $true -or
|
|
$Module.configurationFingerprint -isnot [string] -or
|
|
[string]$Module.configurationFingerprint -cnotmatch
|
|
'^[a-f0-9]{64}$' -or
|
|
$Module.contractFingerprint -isnot [string] -or
|
|
[string]$Module.contractFingerprint -cnotmatch '^[a-f0-9]{64}$' -or
|
|
-not (Test-ExactProperties $Module.inputModeCounts @(
|
|
'scalar', 'lookupSingle', 'unsupported')) -or
|
|
-not (Test-ExactProperties $Module.nativeExecutionProfiles @(
|
|
'create', 'update')) -or
|
|
$Module.genericWriteExecutionAvailable -isnot [bool] -or
|
|
$Module.writeExecutionBlocked -isnot [bool] -or
|
|
$Module.genericUpdateExecutionAvailable -isnot [bool] -or
|
|
$Module.updateExecutionBlocked -isnot [bool] -or
|
|
$Module.planExecutionAllowed -isnot [bool] -or
|
|
$Module.planExecutionAllowed -ne $false) {
|
|
throw $message
|
|
}
|
|
|
|
$masterCount = Read-StrictJsonInt32 `
|
|
$Module.masterParameterCount 0 512 $message
|
|
$detailCount = Read-StrictJsonInt32 `
|
|
$Module.detailParameterCount 0 512 $message
|
|
$totalCount = $masterCount + $detailCount
|
|
if ($totalCount -gt 512 -or
|
|
([string]$Module.moduleKind -ceq 'master_data' -and
|
|
$detailCount -ne 0)) {
|
|
throw $message
|
|
}
|
|
$scalarCount = Read-StrictJsonInt32 `
|
|
$Module.inputModeCounts.scalar 0 $totalCount $message
|
|
$lookupCount = Read-StrictJsonInt32 `
|
|
$Module.inputModeCounts.lookupSingle 0 $totalCount $message
|
|
$unsupportedCount = Read-StrictJsonInt32 `
|
|
$Module.inputModeCounts.unsupported 0 $totalCount $message
|
|
$editableCount = Read-StrictJsonInt32 `
|
|
$Module.editableParameterCount 0 $totalCount $message
|
|
$requiredCount = Read-StrictJsonInt32 `
|
|
$Module.requiredParameterCount 0 $totalCount $message
|
|
$dedicatedCount = Read-StrictJsonInt32 `
|
|
$Module.dedicatedAdapterParameterCount 0 $totalCount $message
|
|
if ($scalarCount + $lookupCount + $unsupportedCount -ne $totalCount -or
|
|
$requiredCount -gt $editableCount -or
|
|
$editableCount -gt $scalarCount + $lookupCount -or
|
|
$dedicatedCount -ne $unsupportedCount) {
|
|
throw $message
|
|
}
|
|
|
|
$moduleKind = [string]$Module.moduleKind
|
|
Assert-ReadOnlyNativeExecutionProfile `
|
|
$Module.nativeExecutionProfiles.create 'create' $moduleKind
|
|
Assert-ReadOnlyNativeExecutionProfile `
|
|
$Module.nativeExecutionProfiles.update 'update' $moduleKind
|
|
Assert-ReadOnlyPayloadContract `
|
|
$Module.payloadContract `
|
|
$moduleKind `
|
|
$masterCount `
|
|
$detailCount `
|
|
$editableCount `
|
|
$requiredCount
|
|
|
|
$writeAvailable = [bool]$Module.genericWriteExecutionAvailable
|
|
if ([bool]$Module.writeExecutionBlocked -ne (-not $writeAvailable) -or
|
|
$Module.writeReadinessCode -isnot [string] -or
|
|
[string]$Module.writeReadinessCode -cnotmatch
|
|
'^[a-z0-9_.-]{1,128}$' -or
|
|
($writeAvailable -and
|
|
($Module.writeCommand -isnot [string] -or
|
|
[string]$Module.writeCommand -cne 'module.record.create' -or
|
|
[string]$Module.writeReadinessCode -cne
|
|
'dynamic_module_write_ready' -or
|
|
$Module.nativeExecutionProfiles.create.available -ne $true)) -or
|
|
(-not $writeAvailable -and
|
|
($null -ne $Module.writeCommand -or
|
|
[string]$Module.writeReadinessCode -ceq
|
|
'dynamic_module_write_ready'))) {
|
|
throw $message
|
|
}
|
|
|
|
$updateAvailable = [bool]$Module.genericUpdateExecutionAvailable
|
|
if ([bool]$Module.updateExecutionBlocked -ne (-not $updateAvailable) -or
|
|
$Module.updateReadinessCode -isnot [string] -or
|
|
[string]$Module.updateReadinessCode -cnotmatch
|
|
'^[a-z0-9_.-]{1,128}$' -or
|
|
($updateAvailable -and
|
|
($Module.updateCommand -isnot [string] -or
|
|
[string]$Module.updateCommand -cne
|
|
'module.record.resolve-update' -or
|
|
[string]$Module.updateReadinessCode -cne
|
|
'dynamic_module_update_ready' -or
|
|
$Module.nativeExecutionProfiles.update.available -ne $true -or
|
|
$moduleKind -ceq 'document')) -or
|
|
(-not $updateAvailable -and
|
|
($null -ne $Module.updateCommand -or
|
|
[string]$Module.updateReadinessCode -ceq
|
|
'dynamic_module_update_ready')) -or
|
|
($moduleKind -ceq 'document' -and
|
|
[string]$Module.updateReadinessCode -cne
|
|
'dynamic_module_update_bill_unsupported')) {
|
|
throw $message
|
|
}
|
|
}
|
|
|
|
function Assert-CommercialPreflightChecks([object]$Report) {
|
|
$checks = @($Report.checks)
|
|
if ($checks.Count -lt $script:requiredPreflightChecks.Count -or
|
|
$checks.Count -gt 64) {
|
|
throw 'Commercial preflight check count is invalid.'
|
|
}
|
|
$names = New-Object 'System.Collections.Generic.HashSet[string]' `
|
|
([StringComparer]::Ordinal)
|
|
foreach ($check in $checks) {
|
|
if (-not (Test-ExactProperties $check @('name', 'passed', 'code', 'detail'))) {
|
|
throw 'Commercial preflight check shape is invalid.'
|
|
}
|
|
$name = [string]$check.name
|
|
$detail = [string]$check.detail
|
|
if ($name -cnotmatch '^[a-z][a-z0-9_]{0,127}$' -or
|
|
-not $names.Add($name) -or
|
|
$check.passed -ne $true -or
|
|
[string]$check.code -cne 'ok' -or
|
|
[string]::IsNullOrWhiteSpace($detail) -or
|
|
$detail.Length -gt 1000) {
|
|
throw 'Commercial preflight contains a duplicate or failed check.'
|
|
}
|
|
}
|
|
foreach ($required in $script:requiredPreflightChecks) {
|
|
if (-not $names.Contains($required)) {
|
|
throw "Commercial preflight is missing required gate: $required"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Invoke-JsonCli([string[]]$Arguments, [string]$Label) {
|
|
$output = @(& $script:cliPath @Arguments 2>&1)
|
|
$exitCode = $LASTEXITCODE
|
|
$text = (($output | ForEach-Object { [string]$_ }) -join [Environment]::NewLine)
|
|
if ($exitCode -ne 0) { throw "$Label failed strict CLI verification." }
|
|
try { $response = $text | ConvertFrom-Json }
|
|
catch { throw "$Label verifier did not return JSON." }
|
|
if (-not (Test-ExactProperties $response @('ok', 'correlationId', 'data')) -or
|
|
$response.ok -ne $true) {
|
|
throw "$Label verifier response envelope is invalid."
|
|
}
|
|
return $response.data
|
|
}
|
|
|
|
function Verify-UatAuthorization(
|
|
[string]$Workflow,
|
|
[string]$ModuleCode,
|
|
[string]$SubSystemId,
|
|
[object]$Artifact,
|
|
[string]$RuntimeHash,
|
|
[string]$CustomerProfileHash,
|
|
[string]$RolloutHash,
|
|
[string]$RuntimeCliVersion,
|
|
[string]$RuntimeCliSha256,
|
|
[string]$RuntimeCliSignerThumbprint,
|
|
[string]$CliSha256,
|
|
[string]$LegacyErpSha256,
|
|
[string]$LegacySignerThumbprint) {
|
|
$value = Invoke-JsonCli @(
|
|
'acceptance', 'verify-uat-authorization', '--input', $Artifact.path
|
|
) "$Workflow UAT authorization"
|
|
if ($value.packageType -cne 'workflow_write_uat_authorization' -or
|
|
$value.schemaVersion -cne '1.2' -or
|
|
$value.sourceSha256 -cne $Artifact.sha256 -or
|
|
$value.customerId -cne $RolloutCustomerId -or
|
|
$value.erpScope.accountBook -cne $AccountBook -or
|
|
$value.erpScope.subSystemId -cne $SubSystemId -or
|
|
$value.erpScope.databaseScopeFingerprint -cne
|
|
$DatabaseScopeFingerprint.ToLowerInvariant() -or
|
|
$value.runtimeConfigurationSha256 -cne $RuntimeHash -or
|
|
$value.customerProfileSha256 -cne $CustomerProfileHash -or
|
|
$value.rolloutPolicySha256 -cne $RolloutHash -or
|
|
$value.sourceCommit -cne $ExpectedSourceCommit.ToLowerInvariant() -or
|
|
$value.packageSha256 -cne $ExpectedPackageSha256.ToLowerInvariant() -or
|
|
$value.runtimeCli.fileName -cne 'lserp-agent-cli.exe' -or
|
|
$value.runtimeCli.version -cne $RuntimeCliVersion -or
|
|
$value.runtimeCli.sha256 -cne $RuntimeCliSha256 -or
|
|
$value.runtimeCli.signerThumbprint -cne
|
|
$RuntimeCliSignerThumbprint -or
|
|
$value.runtimeCli.requiresElevation -ne $false -or
|
|
$value.runtimeCli.bridgeOnly -ne $true -or
|
|
$value.runtimeCli.databaseDirectAccess -ne $false -or
|
|
$value.runtimeCli.sessionSource -cne
|
|
'current_logged_in_erp_process' -or
|
|
$value.verifierCli.sha256 -cne $CliSha256 -or
|
|
$value.verifierCli.signerThumbprint -cne $LegacySignerThumbprint -or
|
|
$value.erpExecutable.sha256 -cne $LegacyErpSha256 -or
|
|
$value.erpExecutable.signerThumbprint -cne $LegacySignerThumbprint -or
|
|
$value.signatureVerified -ne $true -or
|
|
$value.uatAuthorized -ne $true -or
|
|
$value.productionReady -ne $false -or
|
|
@($value.workflows).Count -ne 1 -or
|
|
@($value.workflows | Where-Object {
|
|
[string]$_.workflow -ceq $Workflow -and
|
|
[string]$_.moduleCode -ceq $ModuleCode
|
|
}).Count -ne 1) {
|
|
throw "$Workflow UAT authorization is not independently bound to this final customer release."
|
|
}
|
|
return $value
|
|
}
|
|
|
|
function Read-SessionEvidencePair(
|
|
[string]$Label,
|
|
[object]$PreflightArtifact,
|
|
[object]$HandoffArtifact,
|
|
[Text.Encoding]$StrictUtf8) {
|
|
try {
|
|
$preflight = [IO.File]::ReadAllText(
|
|
$PreflightArtifact.path,
|
|
$StrictUtf8) | ConvertFrom-Json
|
|
$handoff = [IO.File]::ReadAllText(
|
|
$HandoffArtifact.path,
|
|
$StrictUtf8) | ConvertFrom-Json
|
|
$preflightGeneratedAt = (
|
|
[DateTime]$preflight.generatedAtUtc).ToUniversalTime()
|
|
$handoffGeneratedAt = (
|
|
[DateTime]$handoff.generatedAtUtc).ToUniversalTime()
|
|
}
|
|
catch { throw "$Label session evidence is not JSON with UTC timestamps." }
|
|
if ([string]$preflight.schemaVersion -cne '1.5' -or
|
|
[string]$preflight.evidenceType -cne
|
|
'lserp_readonly_session_preflight' -or
|
|
$preflight.passed -ne $true -or
|
|
$preflight.readOnlySessionReady -ne $true -or
|
|
$preflight.productionWriteAuthorized -ne $false -or
|
|
-not (Test-ExactProperties $preflight.cli @(
|
|
'component', 'version', 'protocolVersion', 'bridgeOnly',
|
|
'databaseDirectAccess', 'sessionSource', 'sha256',
|
|
'signerThumbprint')) -or
|
|
[string]$preflight.cli.component -cne 'lserp-agent-cli' -or
|
|
[string]$preflight.cli.version -cne $ExpectedRuntimeCliVersion -or
|
|
[string]$preflight.cli.protocolVersion -cne '1.0' -or
|
|
$preflight.cli.bridgeOnly -ne $true -or
|
|
$preflight.cli.databaseDirectAccess -ne $false -or
|
|
[string]$preflight.cli.sessionSource -cne
|
|
'current_logged_in_erp_process' -or
|
|
[string]$preflight.cli.sha256 -cne $runtimeCliSha256 -or
|
|
[string]$preflight.cli.signerThumbprint -cne
|
|
$runtimeCliSignerThumbprint -or
|
|
[string]$handoff.schemaVersion -cne '1.1' -or
|
|
[string]$handoff.evidenceType -cne
|
|
'lserp_field_readonly_validation_handoff' -or
|
|
$handoff.passed -ne $true -or
|
|
[string]$handoff.validationStage -cne 'final' -or
|
|
-not (Test-ExactProperties $handoff.runtimeCli @(
|
|
'component', 'version', 'sha256', 'signerThumbprint')) -or
|
|
[string]$handoff.runtimeCli.component -cne 'lserp-agent-cli' -or
|
|
[string]$handoff.runtimeCli.version -cne
|
|
$ExpectedRuntimeCliVersion -or
|
|
[string]$handoff.runtimeCli.sha256 -cne $runtimeCliSha256 -or
|
|
[string]$handoff.runtimeCli.signerThumbprint -cne
|
|
$runtimeCliSignerThumbprint -or
|
|
[string]$handoff.preflightEvidenceSha256 -cne
|
|
$PreflightArtifact.sha256 -or
|
|
[int]$handoff.erpProcessId -ne [int]$preflight.erpProcessId -or
|
|
$handoffGeneratedAt -lt $preflightGeneratedAt.AddSeconds(-1)) {
|
|
throw "$Label session evidence pair is not internally bound."
|
|
}
|
|
return [pscustomobject]@{
|
|
preflight = $preflight
|
|
handoff = $handoff
|
|
preflightGeneratedAt = $preflightGeneratedAt
|
|
handoffGeneratedAt = $handoffGeneratedAt
|
|
}
|
|
}
|
|
|
|
function Verify-Workflow(
|
|
[string]$Workflow,
|
|
[string]$ModuleCode,
|
|
[object]$AcceptanceArtifact,
|
|
[object]$WriteArtifact,
|
|
[string]$RuntimeHash,
|
|
[string]$CustomerProfileHash,
|
|
[string]$SubSystemId,
|
|
[object]$UatAuthorization) {
|
|
$acceptance = Invoke-JsonCli @(
|
|
'adapters', 'verify-acceptance-evidence', '--input', $AcceptanceArtifact.path
|
|
) "$Workflow acceptance"
|
|
if ($acceptance.packageType -ne 'workflow_write_acceptance_evidence' -or
|
|
$acceptance.schemaVersion -ne '1.1' -or
|
|
$acceptance.workflow -ne $Workflow -or
|
|
$acceptance.moduleCode -ne $ModuleCode -or
|
|
$acceptance.erpScope.accountBook -ne $AccountBook -or
|
|
$acceptance.erpScope.subSystemId -ne $SubSystemId -or
|
|
$acceptance.runtimeConfigurationSha256 -ne $RuntimeHash -or
|
|
$acceptance.customerProfileSha256 -ne $CustomerProfileHash -or
|
|
$acceptance.signatureVerified -ne $true -or
|
|
$acceptance.registrationReady -ne $false) {
|
|
throw "$Workflow acceptance is not bound to the requested customer scope."
|
|
}
|
|
$write = Invoke-JsonCli @(
|
|
'adapters', 'verify-write-integration-evidence', '--input', $WriteArtifact.path,
|
|
'--workflow', $Workflow, '--module', $ModuleCode,
|
|
'--account-book', $AccountBook, '--subsystem', $SubSystemId,
|
|
'--runtime-sha256', $RuntimeHash,
|
|
'--source-commit', $ExpectedSourceCommit.ToLowerInvariant(),
|
|
'--package-sha256', $ExpectedPackageSha256.ToLowerInvariant()
|
|
) "$Workflow write integration"
|
|
if ($write.evidenceType -ne 'workflow_write_integration' -or
|
|
$write.schemaVersion -ne '1.6' -or $write.verified -ne $true -or
|
|
$write.registrationReady -ne $false -or $write.workflow -ne $Workflow -or
|
|
$write.moduleCode -ne $ModuleCode -or
|
|
$write.erpScope.accountBook -ne $AccountBook -or
|
|
$write.erpScope.subSystemId -ne $SubSystemId -or
|
|
$write.erpScope.userIdSha256 -cne
|
|
$UatAuthorization.erpScope.userIdSha256 -or
|
|
$write.erpScope.userNameSha256 -cne
|
|
$UatAuthorization.erpScope.userNameSha256 -or
|
|
$write.erpScope.databaseScopeFingerprint -cne
|
|
$UatAuthorization.erpScope.databaseScopeFingerprint -or
|
|
$write.erpScope.isAdministrator -ne
|
|
$UatAuthorization.erpScope.isAdministrator -or
|
|
$write.runtimeConfigurationSha256 -ne $RuntimeHash -or
|
|
-not (Test-ExactProperties $write.runtimeCli @(
|
|
'fileName', 'version', 'sha256', 'signerThumbprint',
|
|
'requiresElevation', 'bridgeOnly', 'databaseDirectAccess',
|
|
'sessionSource')) -or
|
|
$write.runtimeCli.fileName -cne 'lserp-agent-cli.exe' -or
|
|
$write.runtimeCli.version -cne $ExpectedRuntimeCliVersion -or
|
|
$write.runtimeCli.sha256 -cne $runtimeCliSha256 -or
|
|
$write.runtimeCli.signerThumbprint -cne
|
|
$runtimeCliSignerThumbprint -or
|
|
$write.runtimeCli.requiresElevation -ne $false -or
|
|
$write.runtimeCli.bridgeOnly -ne $true -or
|
|
$write.runtimeCli.databaseDirectAccess -ne $false -or
|
|
$write.runtimeCli.sessionSource -cne
|
|
'current_logged_in_erp_process' -or
|
|
$write.runtimeCli.version -cne $UatAuthorization.runtimeCli.version -or
|
|
$write.runtimeCli.sha256 -cne $UatAuthorization.runtimeCli.sha256 -or
|
|
$write.runtimeCli.signerThumbprint -cne
|
|
$UatAuthorization.runtimeCli.signerThumbprint -or
|
|
$write.uatAuthorizationSourceSha256 -ne
|
|
$UatAuthorization.sourceSha256 -or
|
|
$write.uatAuthorizationContentSha256 -ne
|
|
$UatAuthorization.contentSha256 -or
|
|
$write.uatAuthorizationIdSha256 -ne
|
|
$UatAuthorization.authorizationIdSha256 -or
|
|
$write.environmentId -cne $UatAuthorization.environmentId -or
|
|
$write.sourceCommit -ne $ExpectedSourceCommit.ToLowerInvariant() -or
|
|
$write.packageSha256 -ne $ExpectedPackageSha256.ToLowerInvariant()) {
|
|
throw "$Workflow write integration evidence is not bound to this release."
|
|
}
|
|
return [ordered]@{
|
|
workflow = $Workflow
|
|
moduleCode = $ModuleCode
|
|
acceptanceEvidenceId = [string]$acceptance.evidenceId
|
|
acceptanceContentSha256 = [string]$acceptance.contentSha256
|
|
writeIntegrationContentSha256 = [string]$write.contentSha256
|
|
environmentId = [string]$write.environmentId
|
|
testedAtUtc = ([DateTime]$write.testedAtUtc).ToUniversalTime().ToString('o')
|
|
testedBy = [string]$write.testedBy
|
|
}
|
|
}
|
|
|
|
function Verify-Diagnostic(
|
|
[string]$ScenarioCode,
|
|
[object]$Artifact,
|
|
[string]$SubSystemId) {
|
|
$value = Invoke-JsonCli @(
|
|
'diagnostics', 'verify-evidence', '--input', $Artifact.path
|
|
) "$ScenarioCode diagnostic"
|
|
if ($value.evidenceType -ne 'module_initialization_diagnosis' -or
|
|
$value.schemaVersion -ne '1.0' -or $value.integrityValid -ne $true -or
|
|
$value.signatureVerified -ne $false -or
|
|
$value.erpScope.accountBook -ne $AccountBook -or
|
|
$value.erpScope.subSystemId -ne $SubSystemId) {
|
|
throw "$ScenarioCode diagnostic is not bound to the requested customer scope."
|
|
}
|
|
return [ordered]@{
|
|
scenarioCode = $ScenarioCode
|
|
evidenceId = [string]$value.evidenceId
|
|
contentSha256 = [string]$value.contentSha256
|
|
moduleCode = [string]$value.erpScope.moduleCode
|
|
primaryFindingCode = [string]$value.primaryFindingCode
|
|
capturedAtUtc = ([DateTime]$value.capturedAtUtc).ToUniversalTime().ToString('o')
|
|
userId = [string]$value.erpScope.userId
|
|
}
|
|
}
|
|
|
|
function Find-SigningCertificate([string]$Thumbprint) {
|
|
$normalized = ($Thumbprint -replace '\s+', '').ToUpperInvariant()
|
|
foreach ($location in @('CurrentUser', 'LocalMachine')) {
|
|
$path = "Cert:\$location\TrustedPeople\$normalized"
|
|
if (Test-Path -LiteralPath $path) {
|
|
$certificate = Get-Item -LiteralPath $path
|
|
if (-not $certificate.HasPrivateKey) { throw 'Signing certificate has no private key.' }
|
|
if ((Get-Date) -lt $certificate.NotBefore -or (Get-Date) -gt $certificate.NotAfter) {
|
|
throw 'Signing certificate is not currently valid.'
|
|
}
|
|
return $certificate
|
|
}
|
|
}
|
|
throw 'Signing certificate was not found in TrustedPeople.'
|
|
}
|
|
|
|
$evidenceRootPath = [IO.Path]::GetFullPath($EvidenceRoot).TrimEnd([char[]]@('\', '/'))
|
|
if (-not [IO.Directory]::Exists($evidenceRootPath)) { throw 'EvidenceRoot does not exist.' }
|
|
$rootItem = Get-Item -LiteralPath $evidenceRootPath -Force
|
|
if (($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
throw 'EvidenceRoot must not be a reparse point.'
|
|
}
|
|
$cliPath = [IO.Path]::GetFullPath($VerifierCliPath)
|
|
if (-not [IO.File]::Exists($cliPath) -or [IO.Path]::GetFileName($cliPath) -ne 'lserp-cli.exe') {
|
|
throw 'Verifier CLI must be the final lserp-cli.exe.'
|
|
}
|
|
$cliItem = Get-Item -LiteralPath $cliPath -Force
|
|
if ($cliItem.Length -le 0 -or $cliItem.Length -gt 64MB -or
|
|
(($cliItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)) {
|
|
throw 'Verifier CLI must be a non-empty ordinary file no larger than 64 MB.'
|
|
}
|
|
$cliArtifact = [ordered]@{ path = $cliPath }
|
|
$runtimeCliPath = [IO.Path]::GetFullPath($RuntimeCliPath)
|
|
if (-not [IO.File]::Exists($runtimeCliPath) -or
|
|
[IO.Path]::GetFileName($runtimeCliPath) -ne 'lserp-agent-cli.exe' -or
|
|
$runtimeCliPath -ieq $cliPath) {
|
|
throw 'Runtime CLI must be the distinct final lserp-agent-cli.exe.'
|
|
}
|
|
$runtimeCliItem = Get-Item -LiteralPath $runtimeCliPath -Force
|
|
if ($runtimeCliItem.Length -le 0 -or $runtimeCliItem.Length -gt 128MB -or
|
|
(($runtimeCliItem.Attributes -band
|
|
[IO.FileAttributes]::ReparsePoint) -ne 0)) {
|
|
throw 'Runtime CLI must be a non-empty ordinary file no larger than 128 MB.'
|
|
}
|
|
$runtimeCliArtifact = [ordered]@{ path = $runtimeCliPath }
|
|
$script:runtimeCliPath = $runtimeCliPath
|
|
|
|
$fileNames = @(
|
|
$PackageFileName, $RuntimeConfigurationFileName, $RolloutPolicyFileName,
|
|
$CustomerProfileFileName,
|
|
$CommercialPreflightFileName,
|
|
$PurchaseReadOnlySessionPreflightFileName,
|
|
$PurchaseFieldReadOnlyValidationHandoffFileName,
|
|
$LeaveReadOnlySessionPreflightFileName,
|
|
$LeaveFieldReadOnlyValidationHandoffFileName,
|
|
$DiagnosisReadOnlySessionPreflightFileName,
|
|
$DiagnosisFieldReadOnlyValidationHandoffFileName,
|
|
$MiniMaxVisionProbeFileName,
|
|
$LegacyBuildEvidenceFileName, $PurchaseAcceptanceFileName,
|
|
$PurchaseWorkflowUatAuthorizationFileName,
|
|
$LeaveWorkflowUatAuthorizationFileName,
|
|
$PurchaseWriteIntegrationFileName, $LeaveAcceptanceFileName,
|
|
$LeaveWriteIntegrationFileName, $DiagnosticMissingFieldFileName,
|
|
$DiagnosticPermissionDeniedFileName, $DiagnosticInvalidRelationFileName,
|
|
$DiagnosticInitializationSqlFailureFileName, $OutputFileName
|
|
)
|
|
if (@($fileNames | Sort-Object -Unique).Count -ne $fileNames.Count) {
|
|
throw 'All evidence and output filenames must be distinct.'
|
|
}
|
|
|
|
$package = Get-Artifact 'Package' $PackageFileName 4GB
|
|
$runtime = Get-Artifact 'Runtime configuration' $RuntimeConfigurationFileName 64KB
|
|
$customerProfile = Get-Artifact 'Customer profile' $CustomerProfileFileName 1MB
|
|
$rollout = Get-Artifact 'Command rollout policy' $RolloutPolicyFileName 256KB
|
|
$preflight = Get-Artifact 'Commercial preflight' $CommercialPreflightFileName 4MB
|
|
$purchaseReadOnlySessionPreflight = Get-Artifact `
|
|
'Purchase ERP read-only session preflight' `
|
|
$PurchaseReadOnlySessionPreflightFileName 2MB
|
|
$purchaseFieldReadOnlyValidationHandoff = Get-Artifact `
|
|
'Purchase field read-only validation handoff' `
|
|
$PurchaseFieldReadOnlyValidationHandoffFileName 256KB
|
|
$leaveReadOnlySessionPreflight = Get-Artifact `
|
|
'Leave ERP read-only session preflight' `
|
|
$LeaveReadOnlySessionPreflightFileName 2MB
|
|
$leaveFieldReadOnlyValidationHandoff = Get-Artifact `
|
|
'Leave field read-only validation handoff' `
|
|
$LeaveFieldReadOnlyValidationHandoffFileName 256KB
|
|
$diagnosisReadOnlySessionPreflight = Get-Artifact `
|
|
'Diagnosis ERP read-only session preflight' `
|
|
$DiagnosisReadOnlySessionPreflightFileName 2MB
|
|
$diagnosisFieldReadOnlyValidationHandoff = Get-Artifact `
|
|
'Diagnosis field read-only validation handoff' `
|
|
$DiagnosisFieldReadOnlyValidationHandoffFileName 256KB
|
|
$miniMaxProbe = Get-Artifact 'MiniMax online vision probe' `
|
|
$MiniMaxVisionProbeFileName 64KB
|
|
$legacy = Get-Artifact 'Legacy build evidence' $LegacyBuildEvidenceFileName 8MB
|
|
$purchaseWorkflowUatAuthorization = Get-Artifact `
|
|
'Purchase workflow UAT authorization' `
|
|
$PurchaseWorkflowUatAuthorizationFileName 512KB
|
|
$leaveWorkflowUatAuthorization = Get-Artifact `
|
|
'Leave workflow UAT authorization' `
|
|
$LeaveWorkflowUatAuthorizationFileName 512KB
|
|
$purchaseAcceptance = Get-Artifact 'Purchase acceptance' $PurchaseAcceptanceFileName 256KB
|
|
$purchaseWrite = Get-Artifact 'Purchase write integration' $PurchaseWriteIntegrationFileName 4MB
|
|
$leaveAcceptance = Get-Artifact 'Leave acceptance' $LeaveAcceptanceFileName 256KB
|
|
$leaveWrite = Get-Artifact 'Leave write integration' $LeaveWriteIntegrationFileName 4MB
|
|
$diagnosticMissing = Get-Artifact 'Missing field diagnostic' $DiagnosticMissingFieldFileName 2MB
|
|
$diagnosticPermission = Get-Artifact 'Permission diagnostic' $DiagnosticPermissionDeniedFileName 2MB
|
|
$diagnosticRelation = Get-Artifact 'Invalid relation diagnostic' $DiagnosticInvalidRelationFileName 2MB
|
|
$diagnosticSql = Get-Artifact 'Initialization SQL diagnostic' $DiagnosticInitializationSqlFailureFileName 2MB
|
|
|
|
$artifactLocks = New-Object 'System.Collections.Generic.List[System.IDisposable]'
|
|
try {
|
|
foreach ($artifact in @(
|
|
$cliArtifact, $runtimeCliArtifact, $package, $runtime,
|
|
$customerProfile, $rollout, $preflight,
|
|
$purchaseReadOnlySessionPreflight,
|
|
$purchaseFieldReadOnlyValidationHandoff,
|
|
$leaveReadOnlySessionPreflight,
|
|
$leaveFieldReadOnlyValidationHandoff,
|
|
$diagnosisReadOnlySessionPreflight,
|
|
$diagnosisFieldReadOnlyValidationHandoff,
|
|
$miniMaxProbe, $legacy, $purchaseAcceptance,
|
|
$purchaseWorkflowUatAuthorization,
|
|
$leaveWorkflowUatAuthorization,
|
|
$purchaseWrite, $leaveAcceptance, $leaveWrite, $diagnosticMissing,
|
|
$diagnosticPermission, $diagnosticRelation, $diagnosticSql)) {
|
|
$lock = [IO.File]::Open(
|
|
$artifact.path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)
|
|
$artifactLocks.Add($lock)
|
|
}
|
|
}
|
|
catch {
|
|
foreach ($lock in $artifactLocks) { $lock.Dispose() }
|
|
throw
|
|
}
|
|
|
|
try {
|
|
if ($package.sha256 -ne $ExpectedPackageSha256.ToLowerInvariant()) {
|
|
throw 'Package file SHA-256 does not match ExpectedPackageSha256.'
|
|
}
|
|
$strictUtf8 = New-Object Text.UTF8Encoding($false, $true)
|
|
$cliStream = [IO.FileStream]$artifactLocks[0]
|
|
$cliStream.Position = 0
|
|
$cliSha = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$cliSha256 = ([BitConverter]::ToString(
|
|
$cliSha.ComputeHash($cliStream))).Replace('-', '').ToLowerInvariant()
|
|
}
|
|
finally { $cliSha.Dispose() }
|
|
$cliStream.Position = 0
|
|
$runtimeCliStream = [IO.FileStream]$artifactLocks[1]
|
|
$runtimeCliStream.Position = 0
|
|
$runtimeCliSha = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$runtimeCliSha256 = ([BitConverter]::ToString(
|
|
$runtimeCliSha.ComputeHash($runtimeCliStream))).Replace(
|
|
'-', '').ToLowerInvariant()
|
|
}
|
|
finally { $runtimeCliSha.Dispose() }
|
|
$runtimeCliStream.Position = 0
|
|
$packagedRuntimeCli = Get-PackagedRuntimeCliContract $package.path $strictUtf8
|
|
if ($packagedRuntimeCli.Version -cne $ExpectedRuntimeCliVersion -or
|
|
$packagedRuntimeCli.SizeBytes -ne $runtimeCliStream.Length -or
|
|
$packagedRuntimeCli.Sha256 -cne $runtimeCliSha256) {
|
|
throw 'Runtime CLI is not the Host/lserp-agent-cli.exe from the final package.'
|
|
}
|
|
|
|
try { $legacyJson = [IO.File]::ReadAllText($legacy.path, $strictUtf8) | ConvertFrom-Json }
|
|
catch { throw 'Legacy build evidence is not JSON.' }
|
|
$legacyCliEntries = @($legacyJson.files | Where-Object {
|
|
[string]$_.path -ieq 'Runtime/lserp-cli.exe'
|
|
})
|
|
$legacyErpEntries = @($legacyJson.files | Where-Object {
|
|
[string]$_.path -ieq 'Runtime/Ls_ERP.exe'
|
|
})
|
|
$signedLegacyFiles = @($legacyJson.authenticode.files | ForEach-Object { [string]$_ })
|
|
$legacySignerThumbprint = ([string]$legacyJson.authenticode.certificateThumbprint).Replace(
|
|
' ', '').ToUpperInvariant()
|
|
if ($legacyJson.schemaVersion -ne '1.0' -or
|
|
$legacyJson.buildVerified -ne $true -or
|
|
$legacyJson.sourceWorktreeDirty -ne $false -or
|
|
([string]$legacyJson.sourceCommit).ToLowerInvariant() -ne $ExpectedSourceCommit.ToLowerInvariant() -or
|
|
$legacyJson.authenticode.signed -ne $true -or
|
|
$legacySignerThumbprint -notmatch '^[A-F0-9]{40}$' -or
|
|
@($signedLegacyFiles | Where-Object { $_ -ieq 'lserp-cli.exe' }).Count -ne 1 -or
|
|
@($signedLegacyFiles | Where-Object { $_ -ieq 'Ls_ERP.exe' }).Count -ne 1 -or
|
|
$legacyCliEntries.Count -ne 1 -or
|
|
$legacyErpEntries.Count -ne 1 -or
|
|
-not (Test-ExactProperties $legacyErpEntries[0] @(
|
|
'path', 'sizeBytes', 'sha256')) -or
|
|
-not (Test-ExactProperties $legacyCliEntries[0] @('path', 'sizeBytes', 'sha256')) -or
|
|
[long]$legacyErpEntries[0].sizeBytes -le 0 -or
|
|
([string]$legacyErpEntries[0].sha256).ToLowerInvariant() -cnotmatch `
|
|
'^[a-f0-9]{64}$' -or
|
|
[long]$legacyCliEntries[0].sizeBytes -ne $cliStream.Length -or
|
|
([string]$legacyCliEntries[0].sha256).ToLowerInvariant() -cne $cliSha256) {
|
|
throw 'Final ERP and verifier CLI are not both bound by legacy build evidence.'
|
|
}
|
|
$cliSignature = Get-AuthenticodeSignature -LiteralPath $cliPath
|
|
$actualCliSigner = if ($null -eq $cliSignature.SignerCertificate) {
|
|
''
|
|
} else {
|
|
([string]$cliSignature.SignerCertificate.Thumbprint).Replace(' ', '').ToUpperInvariant()
|
|
}
|
|
if ($cliSignature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or
|
|
$actualCliSigner -cne $legacySignerThumbprint) {
|
|
throw 'Verifier CLI Authenticode signature does not match legacy build evidence.'
|
|
}
|
|
$runtimeCliSignature = Get-AuthenticodeSignature -LiteralPath $runtimeCliPath
|
|
$runtimeCliSignerThumbprint = if (
|
|
$null -eq $runtimeCliSignature.SignerCertificate) {
|
|
''
|
|
} else {
|
|
([string]$runtimeCliSignature.SignerCertificate.Thumbprint).Replace(
|
|
' ', '').ToUpperInvariant()
|
|
}
|
|
if ($runtimeCliSignature.Status -ne
|
|
[System.Management.Automation.SignatureStatus]::Valid -or
|
|
$runtimeCliSignerThumbprint -cne $legacySignerThumbprint) {
|
|
throw 'Runtime CLI Authenticode signature does not match the release signer.'
|
|
}
|
|
$runtimeIdentityCorrelation = 'customer-runtime-version-' +
|
|
[Guid]::NewGuid().ToString('N')
|
|
$runtimeIdentityOutput = @(& $runtimeCliPath version `
|
|
--correlation-id $runtimeIdentityCorrelation 2>&1)
|
|
$runtimeIdentityExitCode = $LASTEXITCODE
|
|
$runtimeIdentityText = (($runtimeIdentityOutput | ForEach-Object {
|
|
[string]$_
|
|
}) -join [Environment]::NewLine)
|
|
try { $runtimeIdentityEnvelope = $runtimeIdentityText | ConvertFrom-Json }
|
|
catch { throw 'Runtime CLI identity response is not JSON.' }
|
|
$runtimeIdentity = $runtimeIdentityEnvelope.data
|
|
if ($runtimeIdentityExitCode -ne 0 -or
|
|
-not (Test-ExactProperties $runtimeIdentityEnvelope @(
|
|
'ok', 'correlationId', 'data')) -or
|
|
$runtimeIdentityEnvelope.ok -ne $true -or
|
|
[string]$runtimeIdentityEnvelope.correlationId -cne
|
|
$runtimeIdentityCorrelation -or
|
|
-not (Test-ExactProperties $runtimeIdentity @(
|
|
'component', 'version', 'protocolVersion', 'bridgeOnly',
|
|
'databaseDirectAccess', 'sessionSource')) -or
|
|
[string]$runtimeIdentity.component -cne 'lserp-agent-cli' -or
|
|
[string]$runtimeIdentity.version -cne $ExpectedRuntimeCliVersion -or
|
|
[string]$runtimeIdentity.protocolVersion -cne '1.0' -or
|
|
$runtimeIdentity.bridgeOnly -ne $true -or
|
|
$runtimeIdentity.databaseDirectAccess -ne $false -or
|
|
[string]$runtimeIdentity.sessionSource -cne
|
|
'current_logged_in_erp_process') {
|
|
throw 'Runtime CLI identity contract is invalid.'
|
|
}
|
|
$runtimeCliStream.Position = 0
|
|
$runtimeCliSha = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$runtimeCliHashAfterIdentity = ([BitConverter]::ToString(
|
|
$runtimeCliSha.ComputeHash($runtimeCliStream))).Replace(
|
|
'-', '').ToLowerInvariant()
|
|
}
|
|
finally { $runtimeCliSha.Dispose() }
|
|
if ($runtimeCliHashAfterIdentity -cne $runtimeCliSha256) {
|
|
throw 'Runtime CLI changed during identity verification.'
|
|
}
|
|
|
|
$legacyErpSha256 = ([string]$legacyErpEntries[0].sha256).ToLowerInvariant()
|
|
$purchaseUatAuthorization = Verify-UatAuthorization `
|
|
'purchase' $PurchaseModuleCode $PurchaseSubSystemId `
|
|
$purchaseWorkflowUatAuthorization $runtime.sha256 `
|
|
$customerProfile.sha256 $rollout.sha256 `
|
|
$ExpectedRuntimeCliVersion $runtimeCliSha256 `
|
|
$runtimeCliSignerThumbprint $cliSha256 `
|
|
$legacyErpSha256 $legacySignerThumbprint
|
|
$leaveUatAuthorization = Verify-UatAuthorization `
|
|
'leave' $LeaveModuleCode $LeaveSubSystemId `
|
|
$leaveWorkflowUatAuthorization $runtime.sha256 `
|
|
$customerProfile.sha256 $rollout.sha256 `
|
|
$ExpectedRuntimeCliVersion $runtimeCliSha256 `
|
|
$runtimeCliSignerThumbprint $cliSha256 `
|
|
$legacyErpSha256 $legacySignerThumbprint
|
|
|
|
$purchaseSessionEvidence = Read-SessionEvidencePair `
|
|
'Purchase' $purchaseReadOnlySessionPreflight `
|
|
$purchaseFieldReadOnlyValidationHandoff $strictUtf8
|
|
$leaveSessionEvidence = Read-SessionEvidencePair `
|
|
'Leave' $leaveReadOnlySessionPreflight `
|
|
$leaveFieldReadOnlyValidationHandoff $strictUtf8
|
|
$diagnosisSessionEvidence = Read-SessionEvidencePair `
|
|
'Diagnosis' $diagnosisReadOnlySessionPreflight `
|
|
$diagnosisFieldReadOnlyValidationHandoff $strictUtf8
|
|
try {
|
|
$preflightRaw = [IO.File]::ReadAllText($preflight.path, $strictUtf8)
|
|
$preflightJson = $preflightRaw | ConvertFrom-Json
|
|
}
|
|
catch { throw 'Commercial preflight report is not JSON.' }
|
|
$preflightProbeTimestampMatches = [Text.RegularExpressions.Regex]::Matches(
|
|
$preflightRaw,
|
|
'"miniMaxVisionProbeObservedAtUtc"\s*:\s*"(?<value>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?\+00:00)"',
|
|
[Text.RegularExpressions.RegexOptions]::CultureInvariant)
|
|
$preflightProbeObservedAt = if ($preflightProbeTimestampMatches.Count -eq 1) {
|
|
$preflightProbeTimestampMatches[0].Groups['value'].Value
|
|
} else { '' }
|
|
if (-not (Test-ExactProperties $preflightJson @(
|
|
'schemaVersion', 'generatedAtUtc', 'passed',
|
|
'packageManifestVerified', 'packageSha256', 'packageSourceCommit',
|
|
'legacyBuildEvidenceSha256', 'rolloutPolicySha256',
|
|
'rolloutCustomerId', 'spriteSha256',
|
|
'spriteLicenseEvidenceSha256', 'astrBotComplianceEvidenceSha256',
|
|
'miniMaxServiceComplianceEvidenceSha256', 'miniMaxIntegrationMode',
|
|
'miniMaxVisionProbeEvidenceSha256',
|
|
'miniMaxVisionProbeObservedAtUtc', 'miniMaxVisionProbeRegion',
|
|
'miniMaxVisionProbeContractVersion',
|
|
'checks', 'nextStep')) -or
|
|
$preflightJson.schemaVersion -ne '1.7' -or
|
|
$preflightJson.passed -ne $true -or $preflightJson.packageManifestVerified -ne $true -or
|
|
([string]$preflightJson.packageSha256).ToLowerInvariant() -ne $package.sha256 -or
|
|
([string]$preflightJson.packageSourceCommit).ToLowerInvariant() -ne $ExpectedSourceCommit.ToLowerInvariant() -or
|
|
([string]$preflightJson.legacyBuildEvidenceSha256).ToLowerInvariant() -ne $legacy.sha256 -or
|
|
([string]$preflightJson.rolloutPolicySha256).ToLowerInvariant() -ne $rollout.sha256 -or
|
|
([string]$preflightJson.rolloutCustomerId) -cne $RolloutCustomerId -or
|
|
([string]$preflightJson.astrBotComplianceEvidenceSha256) -cnotmatch `
|
|
'^[a-f0-9]{64}$' -or
|
|
([string]$preflightJson.miniMaxServiceComplianceEvidenceSha256) -cnotmatch `
|
|
'^[a-f0-9]{64}$' -or
|
|
([string]$preflightJson.miniMaxIntegrationMode) -cne `
|
|
'direct_https_vlm' -or
|
|
([string]$preflightJson.miniMaxVisionProbeEvidenceSha256).ToLowerInvariant() `
|
|
-cne $miniMaxProbe.sha256 -or
|
|
([string]$preflightJson.miniMaxVisionProbeRegion) -cnotin @('cn', 'global') -or
|
|
([string]$preflightJson.miniMaxVisionProbeContractVersion) -cne '0.0.4' -or
|
|
[string]::IsNullOrWhiteSpace($preflightProbeObservedAt)) {
|
|
throw 'Commercial preflight report is not passed or does not bind the same final ZIP, source, legacy evidence, and rollout policy.'
|
|
}
|
|
Assert-CommercialPreflightChecks $preflightJson
|
|
$preflightGeneratedAt = ([DateTime]$preflightJson.generatedAtUtc).ToUniversalTime()
|
|
|
|
try {
|
|
$miniMaxProbeRaw = [IO.File]::ReadAllText($miniMaxProbe.path, $strictUtf8)
|
|
$miniMaxProbeJson = $miniMaxProbeRaw | ConvertFrom-Json
|
|
}
|
|
catch { throw 'MiniMax online vision probe is not JSON.' }
|
|
$miniMaxProbeTimestampMatches = [Text.RegularExpressions.Regex]::Matches(
|
|
$miniMaxProbeRaw,
|
|
'"observedAtUtc"\s*:\s*"(?<value>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?\+00:00)"',
|
|
[Text.RegularExpressions.RegexOptions]::CultureInvariant)
|
|
$miniMaxProbeObservedAt = if ($miniMaxProbeTimestampMatches.Count -eq 1) {
|
|
$miniMaxProbeTimestampMatches[0].Groups['value'].Value
|
|
} else { '' }
|
|
if (-not (Test-ExactProperties $miniMaxProbeJson @(
|
|
'schemaVersion', 'observedAtUtc', 'passed', 'region', 'endpoint',
|
|
'contract', 'syntheticSourceSha256', 'result')) -or
|
|
-not (Test-ExactProperties $miniMaxProbeJson.contract @(
|
|
'component', 'version', 'sourceCommit', 'clientSourceSha256',
|
|
'serverSourceSha256', 'apiSourceHeader')) -or
|
|
-not (Test-ExactProperties $miniMaxProbeJson.result @(
|
|
'schemaVersion', 'documentType', 'lineCount', 'uncertainFieldCount',
|
|
'contentSha256')) -or
|
|
$miniMaxProbeJson.passed -ne $true -or
|
|
([string]$miniMaxProbeJson.schemaVersion) -cne '1.0' -or
|
|
([string]$miniMaxProbeJson.region) -cne `
|
|
([string]$preflightJson.miniMaxVisionProbeRegion) -or
|
|
$miniMaxProbeObservedAt -cne $preflightProbeObservedAt -or
|
|
([string]$miniMaxProbeJson.contract.component) -cne `
|
|
'minimax-coding-plan-mcp' -or
|
|
([string]$miniMaxProbeJson.contract.version) -cne '0.0.4' -or
|
|
([string]$miniMaxProbeJson.contract.sourceCommit) -cne `
|
|
'fbac3b3e56922a1249e00eebe07d9ee68f4768dc' -or
|
|
([string]$miniMaxProbeJson.contract.clientSourceSha256) -cne `
|
|
'08d4116a20e8a652ceb9e2b6f58b1e7cdfe464b14baff05977e08b4b05b66be3' -or
|
|
([string]$miniMaxProbeJson.contract.serverSourceSha256) -cne `
|
|
'1dea28d6ba4ee46ba516d7eeedd325a5a102410bb7abb074fc4b0a8a66571864' -or
|
|
([string]$miniMaxProbeJson.contract.apiSourceHeader) -cne 'Minimax-MCP' -or
|
|
([string]$miniMaxProbeJson.syntheticSourceSha256) -cne `
|
|
'd37476a5273821c12ee4a72b512dc152db5729055b6febb8603985f86243abda') {
|
|
throw 'MiniMax online vision probe does not match the current reviewed contract or commercial preflight.'
|
|
}
|
|
$expectedMiniMaxEndpoint = if ($miniMaxProbeJson.region -ceq 'cn') {
|
|
'https://api.minimaxi.com/v1/coding_plan/vlm'
|
|
} else {
|
|
'https://api.minimax.io/v1/coding_plan/vlm'
|
|
}
|
|
if ([string]$miniMaxProbeJson.endpoint -cne $expectedMiniMaxEndpoint) {
|
|
throw 'MiniMax online vision probe endpoint does not match its fixed region.'
|
|
}
|
|
|
|
$purchaseSummary = Verify-Workflow 'purchase' $PurchaseModuleCode `
|
|
$purchaseAcceptance $purchaseWrite $runtime.sha256 `
|
|
$customerProfile.sha256 $PurchaseSubSystemId $purchaseUatAuthorization
|
|
$leaveSummary = Verify-Workflow 'leave' $LeaveModuleCode `
|
|
$leaveAcceptance $leaveWrite $runtime.sha256 `
|
|
$customerProfile.sha256 $LeaveSubSystemId $leaveUatAuthorization
|
|
$missingSummary = Verify-Diagnostic `
|
|
'missing_field' $diagnosticMissing $DiagnosisSubSystemId
|
|
$permissionSummary = Verify-Diagnostic `
|
|
'permission_denied' $diagnosticPermission $DiagnosisSubSystemId
|
|
$relationSummary = Verify-Diagnostic `
|
|
'invalid_relation' $diagnosticRelation $DiagnosisSubSystemId
|
|
$sqlSummary = Verify-Diagnostic `
|
|
'initialization_sql_failure' $diagnosticSql $DiagnosisSubSystemId
|
|
$diagnosticUsers = @(@(
|
|
$missingSummary.userId, $permissionSummary.userId,
|
|
$relationSummary.userId, $sqlSummary.userId
|
|
) | Sort-Object -Unique)
|
|
if ($diagnosticUsers.Count -ne 1 -or [string]::IsNullOrWhiteSpace($diagnosticUsers[0])) {
|
|
throw 'All four diagnostic scenarios must be captured by the same ERP administrator.'
|
|
}
|
|
$missingSummary.Remove('userId') | Out-Null
|
|
$permissionSummary.Remove('userId') | Out-Null
|
|
$relationSummary.Remove('userId') | Out-Null
|
|
$sqlSummary.Remove('userId') | Out-Null
|
|
|
|
function Artifact-Manifest([object]$Value) {
|
|
return [ordered]@{
|
|
fileName = $Value.fileName
|
|
sizeBytes = [long]$Value.sizeBytes
|
|
sha256 = $Value.sha256
|
|
}
|
|
}
|
|
|
|
$issuedAt = [DateTime]::UtcNow
|
|
$expiresAt = $issuedAt.AddDays($ValidDays)
|
|
$content = [ordered]@{
|
|
packageType = 'customer_commercial_acceptance_bundle'
|
|
sourceCommit = $ExpectedSourceCommit.ToLowerInvariant()
|
|
packageSha256 = $ExpectedPackageSha256.ToLowerInvariant()
|
|
erpScope = [ordered]@{
|
|
accountBook = $AccountBook
|
|
databaseScopeFingerprint =
|
|
$ExpectedDatabaseScopeFingerprint.ToLowerInvariant()
|
|
}
|
|
sessionScopes = [ordered]@{
|
|
purchase = [ordered]@{
|
|
subSystemId = $PurchaseSubSystemId
|
|
readOnlySessionPreflightGeneratedAtUtc =
|
|
$purchaseSessionEvidence.preflightGeneratedAt.ToString('o')
|
|
fieldReadOnlyValidationHandoffGeneratedAtUtc =
|
|
$purchaseSessionEvidence.handoffGeneratedAt.ToString('o')
|
|
}
|
|
leave = [ordered]@{
|
|
subSystemId = $LeaveSubSystemId
|
|
readOnlySessionPreflightGeneratedAtUtc =
|
|
$leaveSessionEvidence.preflightGeneratedAt.ToString('o')
|
|
fieldReadOnlyValidationHandoffGeneratedAtUtc =
|
|
$leaveSessionEvidence.handoffGeneratedAt.ToString('o')
|
|
}
|
|
diagnosis = [ordered]@{
|
|
subSystemId = $DiagnosisSubSystemId
|
|
readOnlySessionPreflightGeneratedAtUtc =
|
|
$diagnosisSessionEvidence.preflightGeneratedAt.ToString('o')
|
|
fieldReadOnlyValidationHandoffGeneratedAtUtc =
|
|
$diagnosisSessionEvidence.handoffGeneratedAt.ToString('o')
|
|
}
|
|
}
|
|
runtimeConfigurationSha256 = $runtime.sha256
|
|
customerProfileSha256 = $customerProfile.sha256
|
|
rolloutPolicySha256 = $rollout.sha256
|
|
rolloutCustomerId = $RolloutCustomerId
|
|
runtimeCliVersion = $ExpectedRuntimeCliVersion
|
|
runtimeCliSha256 = $runtimeCliSha256
|
|
runtimeCliSignerThumbprint = $runtimeCliSignerThumbprint
|
|
verifierCliSha256 = $cliSha256
|
|
verifierSignerThumbprint = $legacySignerThumbprint
|
|
purchaseWorkflowUatAuthorizationSourceSha256 = `
|
|
[string]$purchaseUatAuthorization.sourceSha256
|
|
purchaseWorkflowUatAuthorizationContentSha256 = `
|
|
[string]$purchaseUatAuthorization.contentSha256
|
|
purchaseWorkflowUatAuthorizationIdSha256 = `
|
|
[string]$purchaseUatAuthorization.authorizationIdSha256
|
|
leaveWorkflowUatAuthorizationSourceSha256 = `
|
|
[string]$leaveUatAuthorization.sourceSha256
|
|
leaveWorkflowUatAuthorizationContentSha256 = `
|
|
[string]$leaveUatAuthorization.contentSha256
|
|
leaveWorkflowUatAuthorizationIdSha256 = `
|
|
[string]$leaveUatAuthorization.authorizationIdSha256
|
|
commercialPreflightGeneratedAtUtc = $preflightGeneratedAt.ToString('o')
|
|
miniMaxVisionProbeEvidenceSha256 = $miniMaxProbe.sha256
|
|
miniMaxVisionProbeObservedAtUtc = $preflightProbeObservedAt
|
|
miniMaxVisionProbeRegion = [string]$preflightJson.miniMaxVisionProbeRegion
|
|
miniMaxVisionProbeContractVersion = `
|
|
[string]$preflightJson.miniMaxVisionProbeContractVersion
|
|
workflows = [ordered]@{
|
|
purchase = $purchaseSummary
|
|
leave = $leaveSummary
|
|
}
|
|
diagnostics = [ordered]@{
|
|
missingField = $missingSummary
|
|
permissionDenied = $permissionSummary
|
|
invalidRelation = $relationSummary
|
|
initializationSqlFailure = $sqlSummary
|
|
}
|
|
artifacts = [ordered]@{
|
|
package = Artifact-Manifest $package
|
|
runtimeConfiguration = Artifact-Manifest $runtime
|
|
customerProfile = Artifact-Manifest $customerProfile
|
|
rolloutPolicy = Artifact-Manifest $rollout
|
|
commercialPreflight = Artifact-Manifest $preflight
|
|
purchaseReadOnlySessionPreflight =
|
|
Artifact-Manifest $purchaseReadOnlySessionPreflight
|
|
purchaseFieldReadOnlyValidationHandoff =
|
|
Artifact-Manifest $purchaseFieldReadOnlyValidationHandoff
|
|
leaveReadOnlySessionPreflight =
|
|
Artifact-Manifest $leaveReadOnlySessionPreflight
|
|
leaveFieldReadOnlyValidationHandoff =
|
|
Artifact-Manifest $leaveFieldReadOnlyValidationHandoff
|
|
diagnosisReadOnlySessionPreflight =
|
|
Artifact-Manifest $diagnosisReadOnlySessionPreflight
|
|
diagnosisFieldReadOnlyValidationHandoff =
|
|
Artifact-Manifest $diagnosisFieldReadOnlyValidationHandoff
|
|
miniMaxVisionProbe = Artifact-Manifest $miniMaxProbe
|
|
legacyBuildEvidence = Artifact-Manifest $legacy
|
|
purchaseWorkflowUatAuthorization =
|
|
Artifact-Manifest $purchaseWorkflowUatAuthorization
|
|
purchaseAcceptance = Artifact-Manifest $purchaseAcceptance
|
|
purchaseWriteIntegration = Artifact-Manifest $purchaseWrite
|
|
leaveWorkflowUatAuthorization =
|
|
Artifact-Manifest $leaveWorkflowUatAuthorization
|
|
leaveAcceptance = Artifact-Manifest $leaveAcceptance
|
|
leaveWriteIntegration = Artifact-Manifest $leaveWrite
|
|
diagnosticMissingField = Artifact-Manifest $diagnosticMissing
|
|
diagnosticPermissionDenied = Artifact-Manifest $diagnosticPermission
|
|
diagnosticInvalidRelation = Artifact-Manifest $diagnosticRelation
|
|
diagnosticInitializationSqlFailure = Artifact-Manifest $diagnosticSql
|
|
}
|
|
issuedAtUtc = $issuedAt.ToString('o')
|
|
expiresAtUtc = $expiresAt.ToString('o')
|
|
validatedBy = $ValidatedBy
|
|
note = '客户商用总验收:最终 ZIP、干净源码提交、运行配置、客户只读画像、采购/请假/诊断三个独立 ERP 会话动态参数合同、命令发布策略、MiniMax 合成图片在线探针、两条写链路及四类脱敏诊断证据已绑定。'
|
|
}
|
|
$canonical = $content | ConvertTo-Json -Compress -Depth 20
|
|
$contentBytes = $strictUtf8.GetBytes($canonical)
|
|
$contentHash = Get-Sha256Hex $contentBytes
|
|
$thumbprint = ($CertificateThumbprint -replace '\s+', '').ToUpperInvariant()
|
|
$certificate = Find-SigningCertificate $thumbprint
|
|
$rsa = $certificate.PrivateKey -as [Security.Cryptography.RSACryptoServiceProvider]
|
|
if ($null -eq $rsa) { throw 'Signing certificate must expose an RSA CSP private key.' }
|
|
$sha = [Security.Cryptography.SHA256]::Create()
|
|
try { $digest = $sha.ComputeHash($contentBytes) }
|
|
finally { $sha.Dispose() }
|
|
$signature = $rsa.SignHash($digest, [Security.Cryptography.CryptoConfig]::MapNameToOID('SHA256'))
|
|
$envelope = [ordered]@{
|
|
schemaVersion = '1.8'
|
|
contentSha256 = $contentHash
|
|
signatureAlgorithm = 'rsa-sha256'
|
|
certificateThumbprint = $thumbprint
|
|
signatureBase64 = [Convert]::ToBase64String($signature)
|
|
content = $content
|
|
}
|
|
$body = $strictUtf8.GetBytes(($envelope | ConvertTo-Json -Depth 20) + [Environment]::NewLine)
|
|
$bodyHash = Get-Sha256Hex $body
|
|
$target = Join-Path $evidenceRootPath $OutputFileName
|
|
if ([IO.File]::Exists($target) -or [IO.Directory]::Exists($target)) {
|
|
throw 'Output must be a new file.'
|
|
}
|
|
$temporary = Join-Path $evidenceRootPath `
|
|
('.lserp-customer-acceptance-' + [Guid]::NewGuid().ToString('N') + '.tmp')
|
|
$published = $false
|
|
try {
|
|
$stream = [IO.File]::Open(
|
|
$temporary, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
|
|
try { $stream.Write($body, 0, $body.Length); $stream.Flush() }
|
|
finally { $stream.Dispose() }
|
|
$verified = Invoke-JsonCli @(
|
|
'acceptance', 'verify-customer-bundle', '--input', $temporary,
|
|
'--evidence-root', $evidenceRootPath,
|
|
'--source-commit', $ExpectedSourceCommit.ToLowerInvariant(),
|
|
'--package-sha256', $ExpectedPackageSha256.ToLowerInvariant(),
|
|
'--account-book', $AccountBook,
|
|
'--purchase-subsystem', $PurchaseSubSystemId,
|
|
'--leave-subsystem', $LeaveSubSystemId,
|
|
'--diagnosis-subsystem', $DiagnosisSubSystemId,
|
|
'--database-scope-fingerprint',
|
|
$ExpectedDatabaseScopeFingerprint.ToLowerInvariant()
|
|
) 'Customer acceptance bundle'
|
|
if ($verified.packageType -ne 'customer_commercial_acceptance_bundle' -or
|
|
$verified.schemaVersion -ne '1.8' -or
|
|
$verified.contentSha256 -ne $contentHash -or
|
|
$verified.sourceCommit -ne $ExpectedSourceCommit.ToLowerInvariant() -or
|
|
$verified.packageSha256 -ne $ExpectedPackageSha256.ToLowerInvariant() -or
|
|
$verified.runtimeConfigurationSha256 -ne $runtime.sha256 -or
|
|
$verified.customerProfileSha256 -ne $customerProfile.sha256 -or
|
|
$verified.rolloutPolicySha256 -ne $rollout.sha256 -or
|
|
$verified.rolloutCustomerId -cne $RolloutCustomerId -or
|
|
$verified.runtimeCliVersion -cne $ExpectedRuntimeCliVersion -or
|
|
$verified.runtimeCliSha256 -cne $runtimeCliSha256 -or
|
|
$verified.runtimeCliSignerThumbprint -cne
|
|
$runtimeCliSignerThumbprint -or
|
|
$verified.erpScope.databaseScopeFingerprint -cne
|
|
$ExpectedDatabaseScopeFingerprint.ToLowerInvariant() -or
|
|
$verified.verifierCliSha256 -cne $cliSha256 -or
|
|
$verified.verifierSignerThumbprint -cne $legacySignerThumbprint -or
|
|
$verified.purchaseWorkflowUatAuthorizationSourceSha256 -cne
|
|
$purchaseUatAuthorization.sourceSha256 -or
|
|
$verified.leaveWorkflowUatAuthorizationSourceSha256 -cne
|
|
$leaveUatAuthorization.sourceSha256 -or
|
|
[int]$verified.sessionScopes.purchase.erpProcessId -ne
|
|
[int]$purchaseSessionEvidence.preflight.erpProcessId -or
|
|
[int]$verified.sessionScopes.leave.erpProcessId -ne
|
|
[int]$leaveSessionEvidence.preflight.erpProcessId -or
|
|
[int]$verified.sessionScopes.diagnosis.erpProcessId -ne
|
|
[int]$diagnosisSessionEvidence.preflight.erpProcessId -or
|
|
$verified.miniMaxVisionProbeEvidenceSha256 -cne $miniMaxProbe.sha256 -or
|
|
$verified.miniMaxVisionProbeObservedAtUtc -cne `
|
|
$preflightProbeObservedAt -or
|
|
$verified.miniMaxVisionProbeRegion -cne `
|
|
([string]$preflightJson.miniMaxVisionProbeRegion) -or
|
|
$verified.miniMaxVisionProbeContractVersion -cne '0.0.4' -or
|
|
$verified.erpScope.accountBook -ne $AccountBook -or
|
|
$verified.sessionScopes.purchase.subSystemId -ne $PurchaseSubSystemId -or
|
|
$verified.sessionScopes.leave.subSystemId -ne $LeaveSubSystemId -or
|
|
$verified.sessionScopes.diagnosis.subSystemId -ne $DiagnosisSubSystemId -or
|
|
$verified.signatureVerified -ne $true -or
|
|
$verified.allComponentEvidenceVerified -ne $true -or
|
|
$verified.diagnosticScenarioCount -ne 4 -or
|
|
$verified.artifactCount -ne 23 -or
|
|
$verified.registrationReady -ne $false -or
|
|
(Get-Sha256Hex ([IO.File]::ReadAllBytes($temporary))) -ne $bodyHash) {
|
|
throw 'Final CLI response is not bound to the generated customer acceptance bundle.'
|
|
}
|
|
[IO.File]::Move($temporary, $target)
|
|
$published = $true
|
|
}
|
|
finally {
|
|
if (-not $published -and [IO.File]::Exists($temporary)) {
|
|
[IO.File]::Delete($temporary)
|
|
}
|
|
}
|
|
|
|
[ordered]@{
|
|
outputFile = $target
|
|
contentSha256 = $contentHash
|
|
sourceCommit = $ExpectedSourceCommit.ToLowerInvariant()
|
|
packageSha256 = $ExpectedPackageSha256.ToLowerInvariant()
|
|
runtimeConfigurationSha256 = $runtime.sha256
|
|
customerProfileSha256 = $customerProfile.sha256
|
|
rolloutPolicySha256 = $rollout.sha256
|
|
rolloutCustomerId = $RolloutCustomerId
|
|
databaseScopeFingerprint =
|
|
$ExpectedDatabaseScopeFingerprint.ToLowerInvariant()
|
|
runtimeCliVersion = $ExpectedRuntimeCliVersion
|
|
runtimeCliSha256 = $runtimeCliSha256
|
|
runtimeCliSignerThumbprint = $runtimeCliSignerThumbprint
|
|
verifierCliSha256 = $cliSha256
|
|
verifierSignerThumbprint = $legacySignerThumbprint
|
|
purchaseWorkflowUatAuthorizationSourceSha256 = `
|
|
[string]$purchaseUatAuthorization.sourceSha256
|
|
purchaseWorkflowUatAuthorizationContentSha256 = `
|
|
[string]$purchaseUatAuthorization.contentSha256
|
|
purchaseWorkflowUatAuthorizationIdSha256 = `
|
|
[string]$purchaseUatAuthorization.authorizationIdSha256
|
|
leaveWorkflowUatAuthorizationSourceSha256 = `
|
|
[string]$leaveUatAuthorization.sourceSha256
|
|
leaveWorkflowUatAuthorizationContentSha256 = `
|
|
[string]$leaveUatAuthorization.contentSha256
|
|
leaveWorkflowUatAuthorizationIdSha256 = `
|
|
[string]$leaveUatAuthorization.authorizationIdSha256
|
|
sessionScopes = [ordered]@{
|
|
purchase = [ordered]@{
|
|
subSystemId = $PurchaseSubSystemId
|
|
readOnlySessionPreflightSha256 =
|
|
$purchaseReadOnlySessionPreflight.sha256
|
|
fieldReadOnlyValidationHandoffSha256 =
|
|
$purchaseFieldReadOnlyValidationHandoff.sha256
|
|
erpProcessId = [int]$purchaseSessionEvidence.preflight.erpProcessId
|
|
}
|
|
leave = [ordered]@{
|
|
subSystemId = $LeaveSubSystemId
|
|
readOnlySessionPreflightSha256 =
|
|
$leaveReadOnlySessionPreflight.sha256
|
|
fieldReadOnlyValidationHandoffSha256 =
|
|
$leaveFieldReadOnlyValidationHandoff.sha256
|
|
erpProcessId = [int]$leaveSessionEvidence.preflight.erpProcessId
|
|
}
|
|
diagnosis = [ordered]@{
|
|
subSystemId = $DiagnosisSubSystemId
|
|
readOnlySessionPreflightSha256 =
|
|
$diagnosisReadOnlySessionPreflight.sha256
|
|
fieldReadOnlyValidationHandoffSha256 =
|
|
$diagnosisFieldReadOnlyValidationHandoff.sha256
|
|
erpProcessId = [int]$diagnosisSessionEvidence.preflight.erpProcessId
|
|
}
|
|
}
|
|
miniMaxVisionProbeEvidenceSha256 = $miniMaxProbe.sha256
|
|
miniMaxVisionProbeObservedAtUtc = $preflightProbeObservedAt
|
|
miniMaxVisionProbeRegion = [string]$preflightJson.miniMaxVisionProbeRegion
|
|
miniMaxVisionProbeContractVersion = '0.0.4'
|
|
accountBook = $AccountBook
|
|
purchaseModuleCode = $PurchaseModuleCode
|
|
leaveModuleCode = $LeaveModuleCode
|
|
diagnosticScenarioCount = 4
|
|
artifactCount = 23
|
|
issuedAtUtc = $issuedAt.ToString('o')
|
|
expiresAtUtc = $expiresAt.ToString('o')
|
|
certificateThumbprint = $thumbprint
|
|
nextStep = 'Archive the entire evidence root read-only and rerun lserp-cli acceptance verify-customer-bundle on the customer verification terminal.'
|
|
} | ConvertTo-Json -Depth 5
|
|
}
|
|
finally {
|
|
foreach ($lock in $artifactLocks) { $lock.Dispose() }
|
|
}
|