[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*"(?\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*"(?\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() } }