[CmdletBinding()] param( [string]$PackageRoot = $PSScriptRoot, [string]$PackageArchivePath = '', [AllowEmptyString()] [ValidatePattern('^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}$')] [string]$ExpectedPackageVersion = '', [Parameter(Mandatory = $true)][string]$SpritePath, [Parameter(Mandatory = $true)][string]$SpriteLicenseEvidence, [Parameter(Mandatory = $true)][string]$AstrBotComplianceEvidence, [Parameter(Mandatory = $true)][string]$MiniMaxServiceComplianceEvidence, [Parameter(Mandatory = $true)][string]$MiniMaxVisionProbeEvidence, [Parameter(Mandatory = $true)] [ValidatePattern('^[A-Fa-f0-9]{40}$')] [string]$HostCertificateThumbprint, [string]$AstrBotBaseUrl = 'http://127.0.0.1:6185', [ValidatePattern('^[A-Za-z0-9_.-]{1,128}$')] [string]$CredentialTarget = 'Langsu.Lserp.AstrBot.ApiKey', [string]$BridgeDiscoveryDirectory = "$env:LOCALAPPDATA\Langsu\Lserp\AgentBridge", [Parameter(Mandatory = $true)] [ValidateRange(1, 2147483647)] [int]$ErpProcessId, [Parameter(Mandatory = $true)] [ValidatePattern('^[A-Fa-f0-9]{64}$')] [string]$ExpectedDatabaseScopeFingerprint, [Parameter(Mandatory = $true)] [ValidateLength(1, 256)] [string]$ExpectedUserId, [Parameter(Mandatory = $true)] [ValidateLength(1, 256)] [string]$ExpectedUserName, [Parameter(Mandatory = $true)] [ValidateLength(1, 256)] [string]$ExpectedAccountBook, [Parameter(Mandatory = $true)] [ValidateLength(1, 256)] [string]$ExpectedSubSystemId, [Parameter(Mandatory = $true)] [ValidateSet('true', 'false')] [string]$ExpectedIsAdministrator, [string]$LegacyArtifactRoot = '', [Parameter(Mandatory = $true)][string]$RolloutPolicyPath, [Parameter(Mandatory = $true)] [ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$')] [string]$RolloutCustomerId, [string]$ReportDirectory = "$env:LOCALAPPDATA\Langsu\Lserp\AcceptanceReports" ) Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' $expectedAdministrator = $ExpectedIsAdministrator -ieq 'true' $checks = New-Object System.Collections.Generic.List[object] function Add-Check([string]$Name, [bool]$Passed, [string]$Code, [string]$Detail) { $resultCode = if ($Passed) { 'ok' } else { $Code } $checks.Add([ordered]@{ name = $Name passed = $Passed code = $resultCode detail = $Detail }) } function Test-RegularFile([string]$Path, [long]$MaximumBytes) { if (-not [IO.File]::Exists($Path)) { return $false } $item = Get-Item -LiteralPath $Path -Force return $item.Length -gt 0 -and $item.Length -le $MaximumBytes -and (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -eq 0) } function Get-RegularFileSha256([string]$Path, [long]$MaximumBytes) { $stream = $null $sha256 = $null try { if (-not (Test-RegularFile $Path $MaximumBytes)) { return $null } $stream = [IO.File]::Open( $Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) if ($stream.Length -le 0 -or $stream.Length -gt $MaximumBytes) { return $null } $sha256 = [Security.Cryptography.SHA256]::Create() [byte[]]$digest = $sha256.ComputeHash($stream) return -join @($digest | ForEach-Object { $_.ToString('x2') }) } catch { return $null } finally { if ($null -ne $sha256) { $sha256.Dispose() } if ($null -ne $stream) { $stream.Dispose() } } } function Get-BytesSha256([byte[]]$Bytes) { $sha256 = [Security.Cryptography.SHA256]::Create() try { [byte[]]$digest = $sha256.ComputeHash($Bytes) return -join @($digest | ForEach-Object { $_.ToString('x2') }) } finally { $sha256.Dispose() } } function Get-StreamSha256([IO.Stream]$Stream) { $sha256 = [Security.Cryptography.SHA256]::Create() try { [byte[]]$digest = $sha256.ComputeHash($Stream) return -join @($digest | ForEach-Object { $_.ToString('x2') }) } finally { $sha256.Dispose() } } function Test-PackageArchiveBinding( [string]$ArchivePath, [object]$Manifest, [string]$ManifestSha256) { $result = [ordered]@{ passed = $false code = 'package_archive_binding_failed' detail = '最终 ZIP 必须与当前解包目录的清单、逐文件大小和 SHA-256 完全一致。' sha256 = $null } $stream = $null $archive = $null try { if ([string]::IsNullOrWhiteSpace($ArchivePath)) { $result.code = 'package_archive_required' $result.detail = '必须提供最终未修改的商用 ZIP,不能只预检解包目录。' return $result } $fullPath = [IO.Path]::GetFullPath($ArchivePath) if ([IO.Path]::GetExtension($fullPath) -cne '.zip' -or -not (Test-RegularFile $fullPath 4GB) -or $ManifestSha256 -cnotmatch '^[a-f0-9]{64}$') { throw 'package_archive_input_invalid' } Add-Type -AssemblyName System.IO.Compression -ErrorAction Stop try { Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop } catch { if ($null -eq ('System.IO.Compression.ZipArchive' -as [type])) { throw } } $stream = [IO.File]::Open( $fullPath, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) if ($stream.Length -le 0 -or $stream.Length -gt 4GB) { throw 'package_archive_size_invalid' } $archiveSha256 = Get-StreamSha256 $stream $stream.Position = 0 $archive = [IO.Compression.ZipArchive]::new( $stream, [IO.Compression.ZipArchiveMode]::Read, $true) $expected = New-Object 'System.Collections.Generic.Dictionary[string,object]' ` ([StringComparer]::OrdinalIgnoreCase) foreach ($manifestEntry in @($Manifest.files)) { $relative = [string]$manifestEntry.path if ($expected.ContainsKey($relative)) { throw 'package_archive_manifest_duplicate' } $expected.Add($relative, $manifestEntry) } $seen = New-Object 'System.Collections.Generic.HashSet[string]' ` ([StringComparer]::OrdinalIgnoreCase) $files = New-Object System.Collections.Generic.List[object] $rootName = $null [long]$totalLength = 0 foreach ($entry in $archive.Entries) { $name = [string]$entry.FullName if ([string]::IsNullOrWhiteSpace($name) -or $name.Contains('\') -or $name.StartsWith('/') -or $name.Contains(':') -or $name.Contains('//')) { throw 'package_archive_path_invalid' } $isDirectory = $name.EndsWith('/') $trimmed = if ($isDirectory) { $name.TrimEnd('/') } else { $name } $segments = @($trimmed.Split('/')) if ($segments.Count -lt 1 -or @($segments | Where-Object { [string]::IsNullOrWhiteSpace($_) -or $_ -in @('.', '..') }).Count -ne 0 -or $segments[0] -cnotmatch '^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$') { throw 'package_archive_path_invalid' } if ($null -eq $rootName) { $rootName = $segments[0] } if ($segments[0] -cne $rootName -or -not $seen.Add($trimmed)) { throw 'package_archive_root_or_duplicate_invalid' } [int]$externalAttributes = $entry.ExternalAttributes [int]$unixType = (($externalAttributes -shr 16) -band 0xF000) if ($unixType -eq 0xA000 -or ($externalAttributes -band [int][IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'package_archive_link_forbidden' } $attributeDirectory = ($externalAttributes -band [int][IO.FileAttributes]::Directory) -ne 0 if ($isDirectory) { if ($entry.Length -ne 0) { throw 'package_archive_directory_invalid' } continue } if ($attributeDirectory -or $segments.Count -lt 2 -or $entry.Length -le 0 -or $entry.Length -gt 512MB -or $totalLength -gt 4GB - $entry.Length) { throw 'package_archive_entry_invalid' } $totalLength += $entry.Length $relative = [string]::Join('/', $segments[1..($segments.Count - 1)]) $files.Add([ordered]@{ relative = $relative; entry = $entry }) } if ([string]::IsNullOrWhiteSpace($rootName) -or $files.Count -ne $expected.Count + 1) { throw 'package_archive_file_count_invalid' } $manifestFound = $false foreach ($archiveFile in $files) { $relative = [string]$archiveFile.relative $entry = $archiveFile.entry $entryStream = $null try { $entryStream = $entry.Open() $actualHash = Get-StreamSha256 $entryStream } finally { if ($null -ne $entryStream) { $entryStream.Dispose() } } if ($relative -ceq 'SHA256SUMS.json') { if ($manifestFound -or $actualHash -cne $ManifestSha256) { throw 'package_archive_manifest_mismatch' } $manifestFound = $true continue } $manifestEntry = $null if (-not $expected.TryGetValue($relative, [ref]$manifestEntry) -or [long]$manifestEntry.sizeBytes -ne [long]$entry.Length -or ([string]$manifestEntry.sha256).ToLowerInvariant() -cne $actualHash) { throw 'package_archive_entry_mismatch' } } if (-not $manifestFound) { throw 'package_archive_manifest_missing' } $result.passed = $true $result.code = 'ok' $result.detail = ("最终 ZIP 已绑定同一清单并逐项验证 {0} 个包内文件。" -f ` $expected.Count) $result.sha256 = $archiveSha256 return $result } catch { return $result } finally { if ($null -ne $archive) { $archive.Dispose() } if ($null -ne $stream) { $stream.Dispose() } } } function Read-UInt24LittleEndian([byte[]]$Bytes, [int]$Offset) { if ($null -eq $Bytes -or $Offset -lt 0 -or $Offset + 3 -gt $Bytes.Length) { throw 'webp_uint24_out_of_range' } return ([uint32]$Bytes[$Offset] -bor ` ([uint32]$Bytes[$Offset + 1] -shl 8) -bor ` ([uint32]$Bytes[$Offset + 2] -shl 16)) } function Get-WebPDimensions([string]$Path, [long]$MaximumBytes) { $stream = $null $reader = $null try { if (-not (Test-RegularFile $Path $MaximumBytes)) { return $null } $stream = [IO.File]::Open( $Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) if ($stream.Length -lt 26 -or $stream.Length -gt $MaximumBytes) { throw 'webp_length_invalid' } $reader = [IO.BinaryReader]::new($stream, [Text.Encoding]::ASCII, $true) $sha256 = [Security.Cryptography.SHA256]::Create() try { [byte[]]$digest = $sha256.ComputeHash($stream) } finally { $sha256.Dispose() } $digestHex = -join @($digest | ForEach-Object { $_.ToString('x2') }) $stream.Position = 0 if ($reader.ReadUInt32() -ne [uint32]0x46464952) { # RIFF throw 'webp_riff_missing' } [uint32]$riffSize = $reader.ReadUInt32() if ([uint64]$riffSize + 8 -ne [uint64]$stream.Length -or $reader.ReadUInt32() -ne [uint32]0x50424557) { # WEBP throw 'webp_container_invalid' } $width = $null $height = $null $seenVp8X = $false $seenBitstream = $false $chunkCount = 0 while ($stream.Position -lt $stream.Length) { $chunkCount++ if ($chunkCount -gt 1024 -or $stream.Length - $stream.Position -lt 8) { throw 'webp_chunk_header_invalid' } [uint32]$chunkType = $reader.ReadUInt32() [uint32]$chunkSize = $reader.ReadUInt32() [long]$payloadStart = $stream.Position [long]$paddedSize = [long]$chunkSize + ([long]$chunkSize -band 1) if ($paddedSize -gt $stream.Length - $payloadStart) { throw 'webp_chunk_size_invalid' } $candidateWidth = $null $candidateHeight = $null if ($chunkType -eq [uint32]0x58385056) { # VP8X if ($seenVp8X -or $chunkCount -ne 1 -or $chunkSize -ne 10) { throw 'webp_vp8x_invalid' } $seenVp8X = $true [byte[]]$header = $reader.ReadBytes(10) if ($header.Length -ne 10 -or ($header[0] -band 0xc3) -ne 0 -or $header[1] -ne 0 -or $header[2] -ne 0 -or $header[3] -ne 0) { throw 'webp_vp8x_header_invalid' } $candidateWidth = 1 + (Read-UInt24LittleEndian $header 4) $candidateHeight = 1 + (Read-UInt24LittleEndian $header 7) } elseif ($chunkType -eq [uint32]0x20385056) { # VP8 + trailing space if ($seenBitstream -or $chunkSize -lt 10) { throw 'webp_vp8_invalid' } $seenBitstream = $true [byte[]]$header = $reader.ReadBytes(10) [uint32]$frameTag = Read-UInt24LittleEndian $header 0 if ($header.Length -ne 10 -or ($frameTag -band 1) -ne 0 -or $header[3] -ne 0x9d -or $header[4] -ne 0x01 -or $header[5] -ne 0x2a) { throw 'webp_vp8_header_invalid' } $candidateWidth = 1 * (([int]$header[6] -bor ` ([int]$header[7] -shl 8)) -band 0x3fff) $candidateHeight = 1 * (([int]$header[8] -bor ` ([int]$header[9] -shl 8)) -band 0x3fff) } elseif ($chunkType -eq [uint32]0x4c385056) { # VP8L if ($seenBitstream -or $chunkSize -lt 5) { throw 'webp_vp8l_invalid' } $seenBitstream = $true [byte[]]$header = $reader.ReadBytes(5) if ($header.Length -ne 5 -or $header[0] -ne 0x2f) { throw 'webp_vp8l_header_invalid' } [uint64]$bits = [uint64]$header[1] -bor ` ([uint64]$header[2] -shl 8) -bor ` ([uint64]$header[3] -shl 16) -bor ` ([uint64]$header[4] -shl 24) if (($bits -shr 29) -ne 0) { throw 'webp_vp8l_version_invalid' } $candidateWidth = 1 + ($bits -band 0x3fff) $candidateHeight = 1 + (($bits -shr 14) -band 0x3fff) } if ($null -ne $candidateWidth) { if ($candidateWidth -le 0 -or $candidateHeight -le 0 -or ($null -ne $width -and ($width -ne $candidateWidth -or $height -ne $candidateHeight))) { throw 'webp_dimensions_inconsistent' } $width = [int]$candidateWidth $height = [int]$candidateHeight } if (($chunkSize -band 1) -ne 0) { $stream.Position = $payloadStart + [long]$chunkSize if ($reader.ReadByte() -ne 0) { throw 'webp_padding_invalid' } } $stream.Position = $payloadStart + $paddedSize } if ($null -eq $width -or $null -eq $height -or -not $seenBitstream) { throw 'webp_dimensions_missing' } return [pscustomobject]@{ width = [int]$width height = [int]$height sha256 = $digestHex } } catch { return $null } finally { if ($null -ne $reader) { $reader.Dispose() } if ($null -ne $stream) { $stream.Dispose() } } } 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-SafeSessionScopeValue([object]$Value) { if ($null -eq $Value) { return $false } $text = [string]$Value if ([string]::IsNullOrWhiteSpace($text) -or $text.Length -gt 256 -or $text -cne $text.Trim()) { return $false } foreach ($character in $text.ToCharArray()) { if ([char]::IsControl($character)) { return $false } } return $true } function Test-ExpectedSessionContext([object]$Data) { if (-not (Test-ExactProperties $Data @( 'userId', 'userName', 'accountBook', 'subSystemId', 'databaseScopeFingerprint', 'subSystemName', 'isAdministrator', 'activeModule', 'openModuleCount', 'openModulesTruncated', 'openModules'))) { return $false } foreach ($name in @( 'userId', 'userName', 'accountBook', 'subSystemId', 'subSystemName')) { if (-not (Test-SafeSessionScopeValue $Data.$name)) { return $false } } $openModuleCount = -1 return ([string]$Data.userId -ceq $ExpectedUserId) -and ([string]$Data.userName -ceq $ExpectedUserName) -and ([string]$Data.accountBook -ceq $ExpectedAccountBook) -and ([string]$Data.subSystemId -ceq $ExpectedSubSystemId) -and ([string]$Data.databaseScopeFingerprint).ToLowerInvariant() -ceq $ExpectedDatabaseScopeFingerprint.ToLowerInvariant() -and ([string]$Data.databaseScopeFingerprint).ToLowerInvariant() -cmatch '^[a-f0-9]{64}$' -and $Data.isAdministrator -is [bool] -and [bool]$Data.isAdministrator -eq $expectedAdministrator -and $Data.openModulesTruncated -is [bool] -and [int]::TryParse( [string]$Data.openModuleCount, [ref]$openModuleCount) -and $openModuleCount -ge 0 -and $openModuleCount -le 10000 -and @($Data.openModules).Count -le 50 } function Invoke-VerifiedBridgeRead([string]$Action) { if ($Action -cnotin @('health', 'context')) { return $null } $correlationId = 'preflight-' + $Action + '-' + [Guid]::NewGuid().ToString('N') $arguments = @( 'bridge', $Action, '--erp-process-id', [string]$ErpProcessId, '--expected-database-scope-fingerprint', $ExpectedDatabaseScopeFingerprint.ToLowerInvariant(), '--expected-user-id', $ExpectedUserId, '--expected-user-name', $ExpectedUserName, '--expected-account-book', $ExpectedAccountBook, '--expected-subsystem-id', $ExpectedSubSystemId, '--expected-is-administrator', $(if ($expectedAdministrator) { 'true' } else { 'false' }), '--correlation-id', $correlationId ) try { $raw = (& $bridgeCliPath @arguments 2>$null | Out-String) if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($raw) -or $raw.Length -gt 4MB) { return $null } $result = $raw | ConvertFrom-Json if (-not (Test-ExactProperties $result @( 'ok', 'correlationId', 'data')) -or $result.ok -ne $true -or [string]$result.correlationId -cne $correlationId -or $null -eq $result.data) { return $null } return $result.data } catch { return $null } } foreach ($expectedScopeValue in @( $ExpectedUserId, $ExpectedUserName, $ExpectedAccountBook, $ExpectedSubSystemId)) { if (-not (Test-SafeSessionScopeValue $expectedScopeValue)) { throw 'expected_erp_session_scope_invalid' } } $ExpectedDatabaseScopeFingerprint = $ExpectedDatabaseScopeFingerprint.ToLowerInvariant() function Remove-JsonWhitespaceOutsideStrings([string]$Json) { if ($null -eq $Json) { return $null } $builder = New-Object Text.StringBuilder $inString = $false $escaped = $false foreach ($character in $Json.ToCharArray()) { if ($inString) { $builder.Append($character) | Out-Null if ($escaped) { $escaped = $false } elseif ($character -eq '\') { $escaped = $true } elseif ($character -eq '"') { $inString = $false } } elseif ($character -eq '"') { $inString = $true $builder.Append($character) | Out-Null } elseif (-not [char]::IsWhiteSpace($character)) { $builder.Append($character) | Out-Null } } if ($inString -or $escaped) { return $null } return $builder.ToString() } function Test-NonNegativeInteger([object]$Value, [long]$Maximum) { if ($null -eq $Value) { return $false } $integerTypes = @( [byte], [sbyte], [int16], [uint16], [int32], [uint32], [int64], [uint64] ) if ($integerTypes -notcontains $Value.GetType()) { return $false } try { [long]$number = $Value return $number -ge 0 -and $number -le $Maximum } catch { return $false } } function Test-MiniMaxVisionProbeEvidence( [string]$EvidencePath, [DateTimeOffset]$NowUtc) { $result = [ordered]@{ passed = $false code = 'minimax_vision_probe_evidence_missing' detail = '必须提供由随包在线探针生成的、24 小时内且不含客户数据的 MiniMax VLM JSON 报告。' sha256 = $null observedAtUtc = $null region = $null contractVersion = $null } try { if ([string]::IsNullOrWhiteSpace($EvidencePath)) { return $result } $fullPath = [IO.Path]::GetFullPath($EvidencePath) if ([IO.Path]::GetExtension($fullPath) -cne '.json' -or -not (Test-RegularFile $fullPath 64KB)) { return $result } $strictUtf8 = New-Object Text.UTF8Encoding($false, $true) $raw = [IO.File]::ReadAllText($fullPath, $strictUtf8) $evidence = $raw | ConvertFrom-Json $contract = $evidence.contract $probeResult = $evidence.result $shapeValid = (Test-ExactProperties $evidence @( 'schemaVersion', 'observedAtUtc', 'passed', 'region', 'endpoint', 'contract', 'syntheticSourceSha256', 'result')) -and (Test-ExactProperties $contract @( 'component', 'version', 'sourceCommit', 'clientSourceSha256', 'serverSourceSha256', 'apiSourceHeader')) -and (Test-ExactProperties $probeResult @( 'schemaVersion', 'documentType', 'lineCount', 'uncertainFieldCount', 'contentSha256')) -and ($evidence.passed -is [bool]) -and $evidence.passed -eq $true -and ([string]$evidence.schemaVersion -ceq '1.0') -and ([string]$contract.component -ceq 'minimax-coding-plan-mcp') -and ([string]$contract.version -ceq '0.0.4') -and ([string]$contract.sourceCommit -ceq 'fbac3b3e56922a1249e00eebe07d9ee68f4768dc') -and ([string]$contract.clientSourceSha256 -ceq '08d4116a20e8a652ceb9e2b6f58b1e7cdfe464b14baff05977e08b4b05b66be3') -and ([string]$contract.serverSourceSha256 -ceq '1dea28d6ba4ee46ba516d7eeedd325a5a102410bb7abb074fc4b0a8a66571864') -and ([string]$contract.apiSourceHeader -ceq 'Minimax-MCP') -and ([string]$evidence.syntheticSourceSha256 -ceq 'd37476a5273821c12ee4a72b512dc152db5729055b6febb8603985f86243abda') -and ([string]$probeResult.schemaVersion -ceq '1.0') -and @('purchase_invoice', 'purchase_detail', 'unknown') -ccontains ([string]$probeResult.documentType) -and (Test-NonNegativeInteger $probeResult.lineCount 10000) -and (Test-NonNegativeInteger $probeResult.uncertainFieldCount 10000) -and ([string]$probeResult.contentSha256 -cmatch '^[a-f0-9]{64}$') $region = [string]$evidence.region $expectedEndpoint = if ($region -ceq 'cn') { 'https://api.minimaxi.com/v1/coding_plan/vlm' } elseif ($region -ceq 'global') { 'https://api.minimax.io/v1/coding_plan/vlm' } else { '' } $shapeValid = $shapeValid -and -not [string]::IsNullOrWhiteSpace( $expectedEndpoint) -and ([string]$evidence.endpoint -ceq $expectedEndpoint) $timestampMatch = [Text.RegularExpressions.Regex]::Match( $raw, '"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) $timestamp = if ($timestampMatch.Success) { $timestampMatch.Groups['value'].Value } else { '' } $observed = [DateTimeOffset]::MinValue $timestampValid = $timestamp -cmatch ` '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?\+00:00$' -and [DateTimeOffset]::TryParse( $timestamp, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::None, [ref]$observed) -and $observed.Offset -eq [TimeSpan]::Zero $shapeValid = $shapeValid -and $timestampValid $canonicalObject = [ordered]@{ contract = [ordered]@{ apiSourceHeader = [string]$contract.apiSourceHeader clientSourceSha256 = [string]$contract.clientSourceSha256 component = [string]$contract.component serverSourceSha256 = [string]$contract.serverSourceSha256 sourceCommit = [string]$contract.sourceCommit version = [string]$contract.version } endpoint = [string]$evidence.endpoint observedAtUtc = $timestamp passed = $true region = $region result = [ordered]@{ contentSha256 = [string]$probeResult.contentSha256 documentType = [string]$probeResult.documentType lineCount = [long]$probeResult.lineCount schemaVersion = [string]$probeResult.schemaVersion uncertainFieldCount = [long]$probeResult.uncertainFieldCount } schemaVersion = [string]$evidence.schemaVersion syntheticSourceSha256 = [string]$evidence.syntheticSourceSha256 } $actualCanonical = Remove-JsonWhitespaceOutsideStrings $raw $expectedCanonical = Remove-JsonWhitespaceOutsideStrings ` ($canonicalObject | ConvertTo-Json -Compress -Depth 8) $shapeValid = $shapeValid -and -not [string]::IsNullOrWhiteSpace($actualCanonical) -and $actualCanonical -ceq $expectedCanonical if (-not $shapeValid) { $result.code = 'minimax_vision_probe_contract_invalid' $result.detail = 'MiniMax 在线探针报告不是当前受审来源、固定合成图片和精确 JSON 合同。' return $result } if ($observed -gt $NowUtc.AddMinutes(5) -or $observed -lt $NowUtc.AddHours(-24)) { $result.code = 'minimax_vision_probe_stale' $result.detail = 'MiniMax 在线探针必须在预检前 24 小时内完成,未来时间最多容忍 5 分钟。' return $result } $sha256 = Get-RegularFileSha256 $fullPath 64KB if ([string]::IsNullOrWhiteSpace($sha256)) { return $result } $result.passed = $true $result.code = 'ok' $result.detail = '合成图片在线探针合同、区域、时效和报告 SHA-256 已验证。' $result.sha256 = $sha256 $result.observedAtUtc = $timestamp $result.region = $region $result.contractVersion = '0.0.4' return $result } catch { $result.code = 'minimax_vision_probe_contract_invalid' $result.detail = 'MiniMax 在线探针报告不是严格 UTF-8 JSON 或合同字段无效。' return $result } } function Test-LegacyBuildEvidence([string]$ArtifactRoot, [string]$ExpectedCommit) { $result = [ordered]@{ passed = $false code = 'legacy_build_evidence_missing' detail = '必须提供 Windows 旧 ERP/CLI 验收构建目录。' evidenceSha256 = $null erpPath = $null cliPath = $null signedPaths = @() certificateThumbprint = $null } try { if ([string]::IsNullOrWhiteSpace($ArtifactRoot) -or $ExpectedCommit -notmatch '^[A-Fa-f0-9]{40}$') { return $result } $legacyRoot = [IO.Path]::GetFullPath($ArtifactRoot).TrimEnd([char[]]@('\', '/')) if (-not [IO.Directory]::Exists($legacyRoot)) { return $result } $rootItem = Get-Item -LiteralPath $legacyRoot -Force if (($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'legacy_root_reparse_forbidden' } $rootPrefix = $legacyRoot + [IO.Path]::DirectorySeparatorChar $evidencePath = Join-Path $legacyRoot 'LEGACY-BUILD-EVIDENCE.json' if (-not (Test-RegularFile $evidencePath 8MB)) { return $result } $strictUtf8 = New-Object Text.UTF8Encoding($false, $true) $evidence = [IO.File]::ReadAllText($evidencePath, $strictUtf8) | ConvertFrom-Json $expectedProperties = @( 'schemaVersion', 'generatedAtUtc', 'buildVerified', 'releaseReadiness', 'sourceCommit', 'sourceWorktreeDirty', 'configuration', 'platform', 'targetFramework', 'devExpressContract', 'dependencies', 'msbuildVersion', 'editbinVersion', 'authenticode', 'erpBinary', 'cliBinary', 'sourceFiles', 'files', 'remainingHardGates' ) $requiredSignedFiles = @( 'Ls_ERP.exe', 'lserp-cli.exe', 'Lskj.AgentBridge.dll', 'Lskj.CommandKernel.dll', 'Lskj.Core.dll' ) $authenticodeFiles = @($evidence.authenticode.files | ForEach-Object { [string]$_ }) $authenticodeShapeValid = (Test-ExactProperties $evidence.authenticode @( 'signed', 'certificateThumbprint', 'certificateStoreLocation', 'timestampUrl', 'files')) -and $evidence.authenticode.signed -eq $true -and ([string]$evidence.authenticode.certificateThumbprint) -match '^[A-Fa-f0-9]{40}$' -and @('CurrentUser', 'LocalMachine') -contains ` ([string]$evidence.authenticode.certificateStoreLocation) -and ([string]$evidence.authenticode.timestampUrl).StartsWith('https://') -and $authenticodeFiles.Count -eq $requiredSignedFiles.Count foreach ($signedFile in $requiredSignedFiles) { if ($authenticodeFiles -notcontains $signedFile) { $authenticodeShapeValid = $false } } $dependencyShapeValid = (Test-ExactProperties $evidence.dependencies @('cefRedistX86')) -and (Test-ExactProperties $evidence.dependencies.cefRedistX86 @( 'packageId', 'version', 'sha256')) -and $evidence.dependencies.cefRedistX86.packageId -eq 'cef.redist.x86' -and $evidence.dependencies.cefRedistX86.version -eq '87.1.13' -and $evidence.dependencies.cefRedistX86.sha256 -eq ` '34dfe2504c1ffaef02eab1f38578701b045439349997b6465fd5dd6659fab021' $binaryShape = @( 'assemblyName', 'machine', 'pe32', 'ilOnly', 'bit32Required', 'largeAddressAware', 'corFlags', 'runtimeVersion' ) if (-not (Test-ExactProperties $evidence $expectedProperties) -or $evidence.schemaVersion -ne '1.0' -or $evidence.buildVerified -ne $true -or $evidence.releaseReadiness -ne $false -or $evidence.sourceWorktreeDirty -ne $false -or ([string]$evidence.sourceCommit).ToLowerInvariant() -ne $ExpectedCommit.ToLowerInvariant() -or $evidence.configuration -ne 'Release' -or $evidence.platform -ne 'x86' -or $evidence.targetFramework -ne 'v4.0' -or $evidence.devExpressContract -ne '15.2' -or $evidence.erpBinary.assemblyName -ne 'Ls_ERP' -or $evidence.cliBinary.assemblyName -ne 'lserp-cli' -or $evidence.erpBinary.machine -ne '0x014c' -or $evidence.cliBinary.machine -ne '0x014c' -or $evidence.erpBinary.pe32 -ne $true -or $evidence.cliBinary.pe32 -ne $true -or $evidence.erpBinary.ilOnly -ne $true -or $evidence.cliBinary.ilOnly -ne $true -or $evidence.erpBinary.bit32Required -ne $true -or $evidence.cliBinary.bit32Required -ne $true -or $evidence.erpBinary.largeAddressAware -ne $true -or -not (Test-ExactProperties $evidence.erpBinary $binaryShape) -or -not (Test-ExactProperties $evidence.cliBinary $binaryShape) -or -not $dependencyShapeValid -or -not $authenticodeShapeValid -or -not ([string]$evidence.erpBinary.runtimeVersion).StartsWith('v4.0') -or -not ([string]$evidence.cliBinary.runtimeVersion).StartsWith('v4.0') -or $null -eq $evidence.sourceFiles -or $evidence.sourceFiles.Count -le 0 -or $evidence.sourceFiles.Count -gt 100 -or $null -eq $evidence.files -or $evidence.files.Count -le 0 -or $evidence.files.Count -gt 2000 -or $null -eq $evidence.remainingHardGates -or $evidence.remainingHardGates.Count -ne 3) { throw 'legacy_evidence_shape_invalid' } $expectedFiles = New-Object ` 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) foreach ($entry in $evidence.files) { if (-not (Test-ExactProperties $entry @('path', 'sizeBytes', 'sha256'))) { throw 'legacy_evidence_file_shape_invalid' } $relative = [string]$entry.path if ([string]::IsNullOrWhiteSpace($relative) -or $relative.Contains('\') -or $relative.StartsWith('/') -or $relative.Split('/') -contains '..' -or -not $expectedFiles.Add($relative) -or ([string]$entry.sha256) -notmatch '^[A-Fa-f0-9]{64}$') { throw 'legacy_evidence_path_invalid' } $full = [IO.Path]::GetFullPath((Join-Path $legacyRoot ` ($relative.Replace('/', [string][IO.Path]::DirectorySeparatorChar)))) if (-not $full.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase) -or -not (Test-RegularFile $full 512MB)) { throw 'legacy_evidence_file_invalid' } $item = Get-Item -LiteralPath $full -Force if ([long]$entry.sizeBytes -ne $item.Length -or (Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash.ToLowerInvariant() -ne ([string]$entry.sha256).ToLowerInvariant()) { throw 'legacy_evidence_hash_mismatch' } } foreach ($sourceEntry in $evidence.sourceFiles) { if (-not (Test-ExactProperties $sourceEntry @('path', 'sha256')) -or ([string]$sourceEntry.path).Contains('\') -or ([string]$sourceEntry.path).Split('/') -contains '..' -or ([string]$sourceEntry.sha256) -notmatch '^[A-Fa-f0-9]{64}$') { throw 'legacy_source_evidence_shape_invalid' } } $reparseDirectories = @(Get-ChildItem -LiteralPath $legacyRoot -Recurse -Directory -Force | Where-Object { ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 }) if ($reparseDirectories.Count -ne 0) { throw 'legacy_reparse_directory_forbidden' } $actualFiles = @(Get-ChildItem -LiteralPath $legacyRoot -Recurse -File -Force | ForEach-Object { $_.FullName.Substring($rootPrefix.Length).Replace('\', '/') } | Where-Object { $_ -ne 'LEGACY-BUILD-EVIDENCE.json' }) foreach ($relative in $actualFiles) { if (-not $expectedFiles.Contains($relative)) { throw 'legacy_unexpected_artifact_file' } } $requiredCefFiles = [ordered]@{ 'Runtime/Xilium.CefGlue.dll' = ` '5057f66d83727e73ce926f918c459fa4a29c9e0a78a2654c09e58210f4355cd7' 'Runtime/libcef.dll' = ` 'a8d4c9974cbdfc989f9c993cba0a388d69e2c5c009661b2c064baaf016e84cfd' 'Runtime/chrome_elf.dll' = ` 'b6194e1b093a000a7b9a883edfd4114a9e79332775970a20a36da1dd1837e8f3' 'Runtime/icudtl.dat' = ` '8364e6c6bf5744357199de0de3f6ba30846ccda70288675b75059e6fd52241f3' 'Runtime/locales/zh-CN.pak' = ` '6be9ef1c87b3162253090c00487238bbf4e5466d235cb49fdb69915a03c480cf' 'Runtime/locales/en-US.pak' = ` 'f9e993df87cad724a36be1efb4f5a71322c9de4d0885419e5f13ca564115dce7' } foreach ($relative in $requiredCefFiles.Keys) { if (-not $expectedFiles.Contains($relative)) { throw 'legacy_cef_runtime_missing' } $full = Join-Path $legacyRoot ` ($relative.Replace('/', [string][IO.Path]::DirectorySeparatorChar)) if ((Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash.ToLowerInvariant() -ne $requiredCefFiles[$relative]) { throw 'legacy_cef_runtime_hash_mismatch' } } if ($actualFiles.Count -ne $expectedFiles.Count -or -not $expectedFiles.Contains('Runtime/Ls_ERP.exe') -or -not $expectedFiles.Contains('Runtime/lserp-cli.exe') -or -not $expectedFiles.Contains('Runtime/Lskj.AgentBridge.dll') -or -not $expectedFiles.Contains('Runtime/Lskj.CommandKernel.dll') -or -not $expectedFiles.Contains('Runtime/Lskj.Core.dll') -or -not $expectedFiles.Contains('MSBUILD.log')) { throw 'legacy_required_artifact_missing' } $result.passed = $true $result.code = 'ok' $result.detail = ("已验证 {0} 个旧 ERP/CLI 构建文件。" -f $expectedFiles.Count) $result.evidenceSha256 = (Get-FileHash -LiteralPath $evidencePath -Algorithm SHA256).Hash.ToLowerInvariant() $result.erpPath = Join-Path $legacyRoot 'Runtime\Ls_ERP.exe' $result.cliPath = Join-Path $legacyRoot 'Runtime\lserp-cli.exe' $result.signedPaths = @($requiredSignedFiles | ForEach-Object { Join-Path $legacyRoot ('Runtime\' + $_) }) $result.certificateThumbprint = ` ([string]$evidence.authenticode.certificateThumbprint).ToUpperInvariant() return $result } catch { $result.code = 'legacy_build_evidence_invalid' $result.detail = '旧 ERP/CLI 构建证据结构、来源绑定或文件哈希无效。' return $result } } function Get-WebView2Version { foreach ($path in @( 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'HKLM:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'HKCU:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' )) { if (Test-Path -LiteralPath $path) { $value = (Get-ItemProperty -LiteralPath $path -Name pv -ErrorAction SilentlyContinue).pv if ($value) { return [string]$value } } } return $null } function Test-TcpEndpoint([Uri]$Uri) { $port = if ($Uri.IsDefaultPort) { if ($Uri.Scheme -eq 'https') { 443 } else { 80 } } else { $Uri.Port } $client = New-Object System.Net.Sockets.TcpClient try { $pending = $client.BeginConnect($Uri.DnsSafeHost, $port, $null, $null) if (-not $pending.AsyncWaitHandle.WaitOne(3000, $false)) { return $false } $client.EndConnect($pending) return $client.Connected } catch { return $false } finally { $client.Dispose() } } function Get-AstrBotRuntimeContract([Uri]$BaseUri) { $result = [ordered]@{ passed = $false runtimeVersion = $null codeVersion = $null detail = '无法验证 AstrBot 运行时与磁盘代码版本。' } $response = $null $stream = $null $memory = $null try { $endpoint = [Uri]::new($BaseUri, '/api/v1/stats/versions') $request = [Net.HttpWebRequest]::CreateHttp($endpoint) $request.Method = 'GET' $request.Accept = 'application/json' $request.AllowAutoRedirect = $false $request.Timeout = 3000 $request.ReadWriteTimeout = 3000 $request.Proxy = $null $response = [Net.HttpWebResponse]$request.GetResponse() if ([int]$response.StatusCode -ne 200 -or [string]::IsNullOrWhiteSpace($response.ContentType) -or -not $response.ContentType.StartsWith( 'application/json', [StringComparison]::OrdinalIgnoreCase)) { throw 'astrbot_version_response_invalid' } $maximumBytes = 32KB if ($response.ContentLength -gt $maximumBytes) { throw 'astrbot_version_response_too_large' } $stream = $response.GetResponseStream() $memory = [IO.MemoryStream]::new() [byte[]]$buffer = New-Object byte[] 4096 [long]$total = 0 while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) { $total += $read if ($total -gt $maximumBytes) { throw 'astrbot_version_response_too_large' } $memory.Write($buffer, 0, $read) } if ($total -le 0) { throw 'astrbot_version_response_empty' } $strictUtf8 = New-Object Text.UTF8Encoding($false, $true) $payload = $strictUtf8.GetString($memory.ToArray()) | ConvertFrom-Json if (-not (Test-ExactProperties $payload @('status', 'message', 'data')) -or -not (Test-ExactProperties $payload.data @( 'webui_version', 'astrbot_version', 'astrbot_code_version')) -or [string]$payload.status -cne 'ok' -or $payload.data.astrbot_version -isnot [string] -or $payload.data.astrbot_code_version -isnot [string]) { throw 'astrbot_version_response_shape_invalid' } $result.runtimeVersion = [string]$payload.data.astrbot_version $result.codeVersion = [string]$payload.data.astrbot_code_version $result.passed = $result.runtimeVersion -ceq '4.27.2' -and $result.codeVersion -ceq '4.27.2' $result.detail = if ($result.passed) { '运行时与磁盘代码版本均为已复核的 AstrBot 4.27.2。' } else { 'AstrBot 运行时或磁盘代码版本不是已复核的 4.27.2。' } return $result } catch { return $result } finally { if ($null -ne $memory) { $memory.Dispose() } if ($null -ne $stream) { $stream.Dispose() } if ($null -ne $response) { $response.Dispose() } } } $runningOnWindows = $env:OS -eq 'Windows_NT' Add-Check 'windows_os' $runningOnWindows 'windows_required' '必须在客户 Windows 10/11 环境运行。' $root = [IO.Path]::GetFullPath($PackageRoot).TrimEnd([char[]]@('\', '/')) + [IO.Path]::DirectorySeparatorChar $bridgeCliPath = Join-Path $root 'Host\lserp-agent-cli.exe' $manifestPath = Join-Path $root 'SHA256SUMS.json' $manifestPassed = $false $manifest = $null $manifestSha256 = $null try { if (-not (Test-RegularFile $manifestPath 4MB)) { throw 'manifest_missing' } $manifestStream = $null $manifestReader = $null try { $manifestStream = [IO.File]::Open( $manifestPath, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) if ($manifestStream.Length -le 0 -or $manifestStream.Length -gt 4MB) { throw 'manifest_size_invalid' } $manifestReader = [IO.BinaryReader]::new($manifestStream) [byte[]]$manifestBytes = $manifestReader.ReadBytes([int]$manifestStream.Length) if ($manifestBytes.Length -ne $manifestStream.Length) { throw 'manifest_read_incomplete' } $manifestSha256 = Get-BytesSha256 $manifestBytes $strictManifestUtf8 = New-Object Text.UTF8Encoding($false, $true) $manifest = $strictManifestUtf8.GetString($manifestBytes) | ConvertFrom-Json } finally { if ($null -ne $manifestReader) { $manifestReader.Dispose() } elseif ($null -ne $manifestStream) { $manifestStream.Dispose() } } if ($manifest.schemaVersion -ne '1.0' -or $null -eq $manifest.files -or $manifest.files.Count -le 0 -or $manifest.files.Count -gt 2000) { throw 'manifest_shape_invalid' } $packageReparseDirectories = @(Get-ChildItem -LiteralPath $root -Recurse -Directory -Force | Where-Object { ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 }) if ($packageReparseDirectories.Count -ne 0) { throw 'manifest_reparse_directory' } $expected = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) foreach ($entry in $manifest.files) { $relative = [string]$entry.path if ([string]::IsNullOrWhiteSpace($relative) -or $relative.Contains('\') -or $relative.StartsWith('/') -or $relative.Split('/') -contains '..' -or -not $expected.Add($relative)) { throw 'manifest_path_invalid' } $full = [IO.Path]::GetFullPath((Join-Path $root ($relative.Replace('/', [string][IO.Path]::DirectorySeparatorChar)))) if (-not $full.StartsWith($root, [StringComparison]::OrdinalIgnoreCase) -or -not (Test-RegularFile $full 512MB)) { throw 'manifest_file_invalid' } $item = Get-Item -LiteralPath $full -Force if ([long]$entry.sizeBytes -ne $item.Length) { throw 'manifest_size_mismatch' } $actual = (Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash.ToLowerInvariant() if ($actual -ne ([string]$entry.sha256).ToLowerInvariant()) { throw 'manifest_hash_mismatch' } } $actualFiles = @(Get-ChildItem -LiteralPath $root -Recurse -File -Force | ForEach-Object { $_.FullName.Substring($root.Length).Replace('\', '/') } | Where-Object { $_ -ne 'SHA256SUMS.json' }) foreach ($relative in $actualFiles) { if (-not $expected.Contains($relative)) { throw 'unexpected_package_file' } } if ($actualFiles.Count -ne $expected.Count) { throw 'manifest_file_count_mismatch' } $manifestPassed = $true Add-Check 'package_hashes' $true 'ok' ("已验证 {0} 个包内文件。" -f $expected.Count) } catch { $manifestFailure = [string]$_.Exception.Message $knownManifestFailures = @( 'manifest_missing', 'manifest_size_invalid', 'manifest_read_incomplete', 'manifest_shape_invalid', 'manifest_path_invalid', 'manifest_file_invalid', 'manifest_size_mismatch', 'manifest_hash_mismatch', 'unexpected_package_file', 'manifest_file_count_mismatch' 'manifest_reparse_directory' ) if ($knownManifestFailures -notcontains $manifestFailure) { $manifestFailure = 'manifest_verification_error' } Add-Check 'package_hashes' $false 'package_integrity_failed' ` ("逐文件大小或 SHA-256 校验失败;分类:{0}。" -f $manifestFailure) } $packageArchiveBinding = if ($manifestPassed) { Test-PackageArchiveBinding $PackageArchivePath $manifest $manifestSha256 } else { [ordered]@{ passed = $false code = 'package_archive_manifest_invalid' detail = '解包目录清单未通过,无法把最终 ZIP 绑定到本次预检。' sha256 = $null } } Add-Check 'package_archive_binding' $packageArchiveBinding.passed ` $packageArchiveBinding.code $packageArchiveBinding.detail $packageCommit = '' $packageVersion = '' $expectedPackageVersionForBinding = [string]$ExpectedPackageVersion $packageVersionBindingPassed = $false $packageProvenancePassed = $false $sqlCompatibilityPassed = $false $gugaSupplyChainAuditPassed = $false try { $buildReportPath = Join-Path $root 'BUILD-VERIFICATION.json' if (-not (Test-RegularFile $buildReportPath 1MB)) { throw 'build_report_missing' } $strictUtf8 = New-Object Text.UTF8Encoding($false, $true) $buildReport = [IO.File]::ReadAllText($buildReportPath, $strictUtf8) | ConvertFrom-Json $packageCommit = ([string]$buildReport.sourceCommit).ToLowerInvariant() $packageVersion = [string]$buildReport.packageVersion if ([string]::IsNullOrWhiteSpace($expectedPackageVersionForBinding)) { $archiveName = [IO.Path]::GetFileName($PackageArchivePath) $archiveMatch = [Text.RegularExpressions.Regex]::Match( $archiveName, '^Lserp-AgentPet-(?[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4})-win-x64\.zip$') if ($archiveMatch.Success) { $expectedPackageVersionForBinding = $archiveMatch.Groups['version'].Value } } $packageVersionBindingPassed = -not [string]::IsNullOrWhiteSpace($expectedPackageVersionForBinding) -and $packageVersion -ceq $expectedPackageVersionForBinding $verification = $buildReport.automatedVerification $deliveryTopology = $buildReport.deliveryTopology $deliveryTopologyValid = $null -ne $deliveryTopology -and (Test-ExactProperties $deliveryTopology @( 'desktopBundleContainsLserpCli', 'desktopBundleContainsBridgeCli', 'bridgeCliPath', 'bridgeCliDatabaseDirectAccess', 'bridgeCliPublishMode', 'desktopBundleContainsLegacyErp', 'legacyArtifactMode', 'legacyBuildTool', 'runtimeRequiresLegacyArtifact')) -and $deliveryTopology.desktopBundleContainsLserpCli -eq $false -and $deliveryTopology.desktopBundleContainsBridgeCli -eq $true -and [string]$deliveryTopology.bridgeCliPath -ceq ` 'Host/lserp-agent-cli.exe' -and $deliveryTopology.bridgeCliDatabaseDirectAccess -eq $false -and [string]$deliveryTopology.bridgeCliPublishMode -ceq ` 'win_x64_single_file_self_contained' -and $deliveryTopology.desktopBundleContainsLegacyErp -eq $false -and [string]$deliveryTopology.legacyArtifactMode -ceq ` 'separate_signed_windows_build' -and [string]$deliveryTopology.legacyBuildTool -ceq ` 'Deployment/Build-LegacyErpAcceptance.ps1' -and $deliveryTopology.runtimeRequiresLegacyArtifact -eq $true $astrbotRuntimeEvidence = $verification.astrbotRuntimeContract $sqlCompatibility = $verification.sqlServerCompatibility100 $sqlCompatibilityPassed = (Test-ExactProperties $sqlCompatibility @( 'passed', 'failed', 'parserPackage', 'parserVersion', 'dialect')) -and [int]$sqlCompatibility.passed -ge 10 -and [int]$sqlCompatibility.failed -eq 0 -and [string]$sqlCompatibility.parserPackage -ceq ` 'Microsoft.SqlServer.TransactSql.ScriptDom' -and [string]$sqlCompatibility.parserVersion -ceq '180.59.2' -and [string]$sqlCompatibility.dialect -ceq 'TSql100' $gugaBuildAudit = $verification.gugaSupplyChainAudit $gugaAuditRelative = 'Deployment/guga-upstream-audit.v1.json' $gugaAuditPath = Join-Path $root ` ($gugaAuditRelative.Replace('/', [IO.Path]::DirectorySeparatorChar)) $gugaAuditSha256 = Get-RegularFileSha256 $gugaAuditPath 64KB $gugaAudit = [IO.File]::ReadAllText( $gugaAuditPath, $strictUtf8) | ConvertFrom-Json $gugaAsset = $gugaAudit.asset $gugaInstaller = $gugaAudit.installer $gugaServiceSource = $gugaAudit.serviceSource $gugaDecision = $gugaAudit.commercialDecision $bannedGugaHashes = @( '3ebd971ba59a0c988a6be0924669b4c5db9234bcc5d17d506e34eba332e6021f', 'f9f715811c26ca610764a7698e28f2e182882f097f4c60a3f00a79dd7530bd20', '1b61ea2af98717b9ebe55beb4c6b820b89e9c42d4fdfeca21cf63ed3ad4e38da' ) $bundledGugaAssets = @($manifest.files | Where-Object { $relativePath = ([string]$_.path).Replace('\', '/') $fileName = [IO.Path]::GetFileName($relativePath).ToLowerInvariant() $fileName -ceq 'spritesheet.webp' -or $fileName.EndsWith('.codex-pet.zip') -or $bannedGugaHashes -ccontains ([string]$_.sha256).ToLowerInvariant() }) $gugaSupplyChainAuditPassed = (Test-ExactProperties $gugaBuildAudit @( 'passed', 'auditFile', 'auditSha256', 'assetId', 'installerPackage', 'installerVersion', 'installerTarballSha256', 'observedSpriteSha256', 'upstreamCommercialLicensePresent', 'assetBundled')) -and $gugaBuildAudit.passed -is [bool] -and $gugaBuildAudit.passed -eq $true -and [string]$gugaBuildAudit.auditFile -ceq $gugaAuditRelative -and [string]$gugaBuildAudit.auditSha256 -ceq $gugaAuditSha256 -and [string]$gugaBuildAudit.assetId -ceq 'guga' -and [string]$gugaBuildAudit.installerPackage -ceq 'codex-pets' -and [string]$gugaBuildAudit.installerVersion -ceq '0.3.0' -and [string]$gugaBuildAudit.installerTarballSha256 -ceq '9ec8bf1ea09e6d8fdc17b33a594a178a9b20bd3dc6decbb22973758394c9c1c7' -and [string]$gugaBuildAudit.observedSpriteSha256 -ceq '1b61ea2af98717b9ebe55beb4c6b820b89e9c42d4fdfeca21cf63ed3ad4e38da' -and $gugaBuildAudit.upstreamCommercialLicensePresent -is [bool] -and $gugaBuildAudit.upstreamCommercialLicensePresent -eq $false -and $gugaBuildAudit.assetBundled -is [bool] -and $gugaBuildAudit.assetBundled -eq $false -and (Test-ExactProperties $gugaAudit @( 'schemaVersion', 'auditedAtUtc', 'asset', 'installer', 'serviceSource', 'commercialDecision')) -and [string]$gugaAudit.schemaVersion -ceq '1.0' -and [string]$gugaAudit.auditedAtUtc -ceq '2026-08-13T19:31:24+00:00' -and (Test-ExactProperties $gugaAsset @( 'id', 'displayName', 'ownerHandle', 'ownerName', 'uploadedAtUtc', 'shareUrl', 'shareDataUrl', 'downloadUrl', 'packageSha256', 'packageSizeBytes', 'manifestSha256', 'spriteSha256', 'spriteSizeBytes', 'atlasSize', 'licenseMetadataPresent', 'licenseFilePresent')) -and [string]$gugaAsset.id -ceq 'guga' -and [string]$gugaAsset.ownerHandle -ceq 'circus' -and [string]$gugaAsset.shareUrl -ceq 'https://codex-pets.net/share/guga' -and [string]$gugaAsset.packageSha256 -ceq $bannedGugaHashes[0] -and [long]$gugaAsset.packageSizeBytes -eq 1946012 -and [string]$gugaAsset.manifestSha256 -ceq $bannedGugaHashes[1] -and [string]$gugaAsset.spriteSha256 -ceq $bannedGugaHashes[2] -and [long]$gugaAsset.spriteSizeBytes -eq 1945586 -and [string]$gugaAsset.atlasSize -ceq '1536x1872' -and $gugaAsset.licenseMetadataPresent -is [bool] -and $gugaAsset.licenseMetadataPresent -eq $false -and $gugaAsset.licenseFilePresent -is [bool] -and $gugaAsset.licenseFilePresent -eq $false -and (Test-ExactProperties $gugaInstaller @( 'packageName', 'version', 'registryUrl', 'tarballUrl', 'tarballSha1', 'tarballSha256', 'npmIntegrity', 'declaredLicense', 'repositoryDeclared', 'defaultApiBase', 'installRoot', 'writtenFiles', 'assetDigestVerification', 'assetSignatureVerification', 'assetLicenseVerification')) -and [string]$gugaInstaller.packageName -ceq 'codex-pets' -and [string]$gugaInstaller.version -ceq '0.3.0' -and [string]$gugaInstaller.tarballSha1 -ceq '82e41349ae63eb9e63099f2e06a56468182e2c90' -and [string]$gugaInstaller.tarballSha256 -ceq '9ec8bf1ea09e6d8fdc17b33a594a178a9b20bd3dc6decbb22973758394c9c1c7' -and [string]$gugaInstaller.declaredLicense -ceq 'MIT' -and $gugaInstaller.repositoryDeclared -is [bool] -and $gugaInstaller.repositoryDeclared -eq $false -and @($gugaInstaller.writtenFiles).Count -eq 2 -and [string]$gugaInstaller.writtenFiles[0] -ceq 'pet.json' -and [string]$gugaInstaller.writtenFiles[1] -ceq 'spritesheet.webp' -and $gugaInstaller.assetDigestVerification -eq $false -and $gugaInstaller.assetSignatureVerification -eq $false -and $gugaInstaller.assetLicenseVerification -eq $false -and (Test-ExactProperties $gugaServiceSource @( 'repository', 'commit', 'softwareLicense', 'licenseSha256', 'termsSourceSha256', 'termsEffectiveDate', 'uploadTermsScope')) -and [string]$gugaServiceSource.repository -ceq 'https://github.com/portons/codex-pet-share' -and [string]$gugaServiceSource.commit -ceq '22725091da2787e8e525c9289cb7826a34be4950' -and [string]$gugaServiceSource.softwareLicense -ceq 'MIT' -and [string]$gugaServiceSource.licenseSha256 -ceq '13e779572adacb503b7e7a0c676571fcd86114a73f6aa000412c24a9a06a97d3' -and [string]$gugaServiceSource.termsSourceSha256 -ceq '70ad12414864566b8cd469a7d2ca39fe60050686cecacc126ff1aca587f790bb' -and [string]$gugaServiceSource.uploadTermsScope -ceq 'public-sharing-through-service' -and (Test-ExactProperties $gugaDecision @( 'status', 'code', 'reason', 'requiredEvidence')) -and [string]$gugaDecision.status -ceq 'external-license-required' -and [string]$gugaDecision.code -ceq 'guga_commercial_license_missing' -and @($gugaDecision.requiredEvidence).Count -eq 7 -and $bundledGugaAssets.Count -eq 0 $astrbotRuntimeEvidenceValid = (Test-ExactProperties $astrbotRuntimeEvidence @( 'schemaVersion', 'passed', 'repository', 'sourceTag', 'sourceCommit', 'runtimeVersion', 'versionSpecifier', 'pluginVersion', 'licenseExpression', 'licenseSha256', 'eulaSha256', 'criticalSourceFilesVerified', 'registeredTools')) -and $astrbotRuntimeEvidence.schemaVersion -ceq '1.1' -and $astrbotRuntimeEvidence.passed -eq $true -and $astrbotRuntimeEvidence.repository -ceq ` 'https://github.com/AstrBotDevs/AstrBot.git' -and $astrbotRuntimeEvidence.sourceTag -ceq 'v4.27.2' -and $astrbotRuntimeEvidence.sourceCommit -ceq ` 'ad4fbfa90ca0c4ac2b30b3250e34dbf8fe7babbf' -and $astrbotRuntimeEvidence.runtimeVersion -ceq '4.27.2' -and $astrbotRuntimeEvidence.versionSpecifier -ceq '==4.27.2' -and $astrbotRuntimeEvidence.pluginVersion -ceq '0.4.0' -and $astrbotRuntimeEvidence.licenseExpression -ceq ` 'AGPL-3.0-or-later' -and $astrbotRuntimeEvidence.licenseSha256 -ceq ` 'ccf7d08f932af3e813848881731113afbb7c80d0fd6d958e8d319002bf344d02' -and $astrbotRuntimeEvidence.eulaSha256 -ceq ` 'c332de7781e87c67d6d3beda463fa04705075a6bae9e52a252f7c639f6defd80' -and [int]$astrbotRuntimeEvidence.criticalSourceFilesVerified -eq 20 -and @($astrbotRuntimeEvidence.registeredTools).Count -eq 3 -and [string]$astrbotRuntimeEvidence.registeredTools[0] -ceq 'erp_get_context' -and [string]$astrbotRuntimeEvidence.registeredTools[1] -ceq ` 'erp_get_capabilities' -and [string]$astrbotRuntimeEvidence.registeredTools[2] -ceq 'erp_plan_command' if ($buildReport.schemaVersion -ne '1.1' -or -not $packageVersionBindingPassed -or $packageCommit -notmatch '^[a-f0-9]{40}$' -or $buildReport.sourceWorktreeDirty -ne $false -or -not $deliveryTopologyValid -or $verification.commandKernel.failed -ne 0 -or $verification.desktopHost.failed -ne 0 -or $verification.petWebUi.failed -ne 0 -or $verification.astrbotPlugin.failed -ne 0 -or $verification.astrbotPlugin.skipped -ne 0 -or -not $astrbotRuntimeEvidenceValid -or -not $gugaSupplyChainAuditPassed -or $verification.deploymentContracts.failed -ne 0 -or -not $sqlCompatibilityPassed -or $verification.commandKernel.passed -le 0 -or $verification.desktopHost.passed -le 0 -or $verification.petWebUi.passed -le 0 -or $verification.astrbotPlugin.passed -le 0 -or $verification.deploymentContracts.passed -le 0 -or $verification.legacyNet40ApiCompile -ne $true -or $verification.winX64SelfContainedPublish -ne $true -or $verification.bridgeCliWinX64SelfContainedPublish -ne $true -or $verification.hostAuthenticode.signed -ne $true -or ([string]$verification.hostAuthenticode.certificateThumbprint).ToUpperInvariant() -ne $HostCertificateThumbprint.ToUpperInvariant()) { throw 'build_report_invalid' } $packageProvenancePassed = $true } catch { $packageProvenancePassed = $false } Add-Check 'package_source_provenance' $packageProvenancePassed ` 'package_source_provenance_invalid' '包必须来自相同干净提交、版本必须与 ExpectedPackageVersion 或标准 ZIP 文件名一致,且所有自动化契约测试为零失败、零跳过。' Add-Check 'guga_supply_chain_audit' $gugaSupplyChainAuditPassed ` 'guga_supply_chain_audit_invalid' ` '包必须携带锁定 npm/服务/素材哈希的 guga 上游审计,且不得夹带在线下载的素材;该审计不代替书面商用授权。' Add-Check 'sqlserver_compatibility100_syntax' $sqlCompatibilityPassed ` 'sqlserver_compatibility100_syntax_invalid' ` '所有随包 SQL 必须通过锁定版本 Microsoft ScriptDom TSql100 解析,并证明不会误接受高版本语法。' $legacyBuildTool = Join-Path $root 'Deployment\Build-LegacyErpAcceptance.ps1' Add-Check 'legacy_build_tool' (Test-RegularFile $legacyBuildTool 2MB) ` 'legacy_build_tool_missing' '商用包必须携带旧 ERP/CLI 的可重复 Windows 构建与取证脚本。' $legacyBuildContractTests = Join-Path $root 'Deployment\Test-DeploymentContracts.ps1' Add-Check 'legacy_build_contract_tests' (Test-RegularFile $legacyBuildContractTests 2MB) ` 'legacy_build_contract_tests_missing' '商用包必须携带旧 ERP/CLI 构建脚本的离线正负契约测试。' $writeEvidenceTools = @( (Join-Path $root 'Deployment\New-WorkflowWriteCasesTemplate.ps1'), (Join-Path $root 'Deployment\New-WorkflowWriteIntegrationEvidence.ps1'), (Join-Path $root 'Deployment\Invoke-WorkflowWriteCaseCapture.ps1'), (Join-Path $root 'Deployment\Invoke-LserpFieldReadOnlyValidation.ps1'), (Join-Path $root 'Deployment\field-readonly-validation.example.json'), (Join-Path $root 'Deployment\Invoke-LserpReadOnlySessionPreflight.ps1'), (Join-Path $root 'Deployment\Invoke-LserpSelectOnlyCatalogSnapshot.ps1'), (Join-Path $root 'Deployment\Invoke-LserpSelectOnlyProfilePreflight.ps1'), (Join-Path $root 'Deployment\New-WorkflowUatAuthorization.ps1'), (Join-Path $root 'Deployment\New-WorkflowWriteUatCampaign.ps1'), (Join-Path $root 'Deployment\Test-WorkflowWriteUatCampaign.ps1'), (Join-Path $root 'Deployment\workflow-write-uat-case-catalog.v1.json'), (Join-Path $root 'Deployment\New-WorkflowAcceptanceEvidence.ps1'), (Join-Path $root 'Deployment\New-CustomerAcceptanceBundle.ps1') ) $writeEvidenceToolsPresent = @($writeEvidenceTools | Where-Object { Test-RegularFile $_ 2MB }).Count -eq $writeEvidenceTools.Count Add-Check 'workflow_write_evidence_tools' $writeEvidenceToolsPresent ` 'workflow_write_evidence_tools_missing' '商用包必须携带经独立审批并锁定哈希的现场输入交接、绑定精确 ERP 会话的只读动态合同预检、UAT 授权、哈希绑定的 32 用例现场目录、活动编排/断点预检、受控采集、写用例模板、严格证据生成器、工作流签名和客户总验收工具。' $workflowUatCaseCatalogPath = Join-Path $root ` 'Deployment\workflow-write-uat-case-catalog.v1.json' $expectedWorkflowUatCaseCatalogSha256 = ` '23eb6c4f308d4904bf3920ed37499f05521beebde9422026f9732983c16002d5' $workflowUatCaseCatalogValid = $false $workflowUatCaseCatalogStream = $null try { if (-not (Test-RegularFile $workflowUatCaseCatalogPath 256KB)) { throw 'catalog_file_invalid' } $workflowUatCaseCatalogStream = [IO.File]::Open( $workflowUatCaseCatalogPath, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) $catalogHash = Get-StreamSha256 $workflowUatCaseCatalogStream if ($catalogHash -cne $expectedWorkflowUatCaseCatalogSha256) { throw 'catalog_hash_mismatch' } $workflowUatCaseCatalogStream.Position = 0 $catalogReader = New-Object IO.StreamReader( $workflowUatCaseCatalogStream, $strictUtf8, $true, 4096, $true) try { $catalogText = $catalogReader.ReadToEnd() } finally { $catalogReader.Dispose() } $catalog = $catalogText | ConvertFrom-Json if (-not (Test-ExactProperties $catalog @( 'schemaVersion', 'packageType', 'safety', 'workflows')) -or [string]$catalog.schemaVersion -cne '1.0' -or [string]$catalog.packageType -cne 'workflow_write_uat_case_catalog' -or -not (Test-ExactProperties $catalog.safety @( 'productionUseProhibited', 'automaticDatabaseOrConfigurationChanges', 'approvedRestorePointRequired', 'containsCredentials', 'containsBusinessIdentifiers', 'executableInstructionsIncluded')) -or $catalog.safety.productionUseProhibited -ne $true -or $catalog.safety.automaticDatabaseOrConfigurationChanges -ne $false -or $catalog.safety.approvedRestorePointRequired -ne $true -or $catalog.safety.containsCredentials -ne $false -or $catalog.safety.containsBusinessIdentifiers -ne $false -or $catalog.safety.executableInstructionsIncluded -ne $false) { throw 'catalog_contract_invalid' } $catalogWorkflows = @($catalog.workflows) if ($catalogWorkflows.Count -ne 2 -or [string]$catalogWorkflows[0].workflow -cne 'purchase' -or [int]$catalogWorkflows[0].caseCount -ne 13 -or @($catalogWorkflows[0].cases).Count -ne 13 -or [string]$catalogWorkflows[1].workflow -cne 'leave' -or [int]$catalogWorkflows[1].caseCount -ne 19 -or @($catalogWorkflows[1].cases).Count -ne 19) { throw 'catalog_coverage_invalid' } $purchaseCommitCatalogCase = @($catalogWorkflows[0].cases)[0] $purchaseCommitPreconditions = [string]::Join( "`n", @($purchaseCommitCatalogCase.preconditions | ForEach-Object { [string]$_ })) $purchaseCommitDbaChecks = [string]::Join( "`n", @($purchaseCommitCatalogCase.dbaReadOnlyChecks | ForEach-Object { [string]$_ })) if ([string]$purchaseCommitCatalogCase.caseCode -cne 'purchase_unique_match_commit' -or $purchaseCommitCatalogCase.sourceDocumentProofRequired -ne $true -or -not $purchaseCommitPreconditions.Contains('脱敏电子 PDF') -or -not $purchaseCommitPreconditions.Contains( 'pdfium_minimax_pages_v1') -or -not $purchaseCommitDbaChecks.Contains('XML v3') -or -not $purchaseCommitDbaChecks.Contains( 'pdfium_minimax_pages_v1')) { throw 'catalog_pdf_evidence_contract_invalid' } $campaignGeneratorSource = [IO.File]::ReadAllText( (Join-Path $root 'Deployment\New-WorkflowWriteUatCampaign.ps1'), $strictUtf8) $campaignCheckerSource = [IO.File]::ReadAllText( (Join-Path $root 'Deployment\Test-WorkflowWriteUatCampaign.ps1'), $strictUtf8) if (-not $campaignGeneratorSource.Contains($expectedWorkflowUatCaseCatalogSha256) -or -not $campaignCheckerSource.Contains($expectedWorkflowUatCaseCatalogSha256)) { throw 'catalog_tool_binding_invalid' } $workflowUatCaseCatalogValid = $true } catch { $workflowUatCaseCatalogValid = $false } finally { if ($null -ne $workflowUatCaseCatalogStream) { $workflowUatCaseCatalogStream.Dispose() } } Add-Check 'workflow_uat_case_catalog' $workflowUatCaseCatalogValid ` 'workflow_uat_case_catalog_invalid' '32 项现场目录必须保持受审原始字节、安全声明和采购 13/请假 19 覆盖,并由活动生成器及恢复检查器共同绑定其 SHA-256。' $legacyEvidence = Test-LegacyBuildEvidence $LegacyArtifactRoot $packageCommit Add-Check 'legacy_build_evidence' $legacyEvidence.passed $legacyEvidence.code $legacyEvidence.detail $legacyErpSignature = $false $legacyCliSignature = $false $legacyCriticalSignatures = $false if ($legacyEvidence.passed -and $runningOnWindows) { try { $criticalValid = $true foreach ($signedPath in $legacyEvidence.signedPaths) { $signature = Get-AuthenticodeSignature -LiteralPath $signedPath if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or $null -eq $signature.SignerCertificate -or $signature.SignerCertificate.Thumbprint -ne $legacyEvidence.certificateThumbprint) { $criticalValid = $false } } $erpSignature = Get-AuthenticodeSignature -LiteralPath $legacyEvidence.erpPath $cliSignature = Get-AuthenticodeSignature -LiteralPath $legacyEvidence.cliPath $legacyErpSignature = $erpSignature.Status -eq ` [System.Management.Automation.SignatureStatus]::Valid $legacyCliSignature = $cliSignature.Status -eq ` [System.Management.Automation.SignatureStatus]::Valid $legacyCriticalSignatures = $criticalValid } catch { $legacyErpSignature = $false $legacyCliSignature = $false $legacyCriticalSignatures = $false } } Add-Check 'legacy_erp_authenticode' $legacyErpSignature ` 'legacy_erp_signature_invalid' '最终 Ls_ERP.exe 必须具有有效 Authenticode 签名。' Add-Check 'legacy_cli_authenticode' $legacyCliSignature ` 'legacy_cli_signature_invalid' '最终 lserp-cli.exe 必须具有有效 Authenticode 签名。' Add-Check 'legacy_bridge_authenticode' $legacyCriticalSignatures ` 'legacy_bridge_signature_invalid' 'AgentBridge、CommandKernel 和 Core 关键程序集必须与 EXE 使用同一有效签名证书。' $hostPath = Join-Path $root 'Host\Lskj.AgentPet.Host.exe' $hostCriticalPaths = @( $hostPath, (Join-Path $root 'Host\Lskj.AgentPet.Host.dll'), (Join-Path $root 'Host\Lskj.AgentPet.Host.Core.dll'), $bridgeCliPath ) $bundledFullCliCount = if ($manifestPassed) { @($manifest.files | Where-Object { [string]$_.path -match '(^|/)lserp-cli[.]exe$' }).Count } else { 1 } $hostPresent = @($hostCriticalPaths | Where-Object { -not (Test-RegularFile $_ 512MB) }).Count -eq 0 -and $bundledFullCliCount -eq 0 -and -not [IO.File]::Exists((Join-Path $root 'Host\lserp-agent-cli.dll')) -and -not [IO.File]::Exists((Join-Path $root 'Host\lserp-agent-cli.deps.json')) -and -not [IO.File]::Exists((Join-Path $root 'Host\lserp-agent-cli.runtimeconfig.json')) Add-Check 'desktop_host' $hostPresent 'host_missing' '自包含桌宠宿主必须存在。' $hostSignaturesValid = $false if ($hostPresent -and $runningOnWindows) { $hostSignaturesValid = $true $expectedHostThumbprint = $HostCertificateThumbprint.ToUpperInvariant() foreach ($criticalPath in $hostCriticalPaths) { $signature = Get-AuthenticodeSignature -LiteralPath $criticalPath if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or $null -eq $signature.SignerCertificate -or $signature.SignerCertificate.Thumbprint.ToUpperInvariant() -ne $expectedHostThumbprint) { $hostSignaturesValid = $false } } } Add-Check 'host_authenticode' $hostSignaturesValid ` 'host_signature_invalid' '桌宠与桥接 CLI 的一方 EXE/DLL 必须使用指定客户发布证书且签名有效。' $bridgeCliIdentityValid = $false if ($hostSignaturesValid -and $runningOnWindows) { try { $identityCorrelation = 'preflight-version-' + ` [Guid]::NewGuid().ToString('N') $identityRaw = (& $bridgeCliPath version ` --correlation-id $identityCorrelation 2>$null | Out-String) if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($identityRaw) -or $identityRaw.Length -gt 64KB) { throw 'bridge_cli_identity_execution_failed' } $identity = $identityRaw | ConvertFrom-Json $identityData = $identity.data $bridgeCliIdentityValid = (Test-ExactProperties $identity @( 'ok', 'correlationId', 'data')) -and $identity.ok -eq $true -and [string]$identity.correlationId -ceq $identityCorrelation -and (Test-ExactProperties $identityData @( 'component', 'version', 'protocolVersion', 'bridgeOnly', 'databaseDirectAccess', 'sessionSource')) -and [string]$identityData.component -ceq 'lserp-agent-cli' -and [string]$identityData.version -ceq $packageVersion -and [string]$identityData.protocolVersion -ceq '1.0' -and $identityData.bridgeOnly -eq $true -and $identityData.databaseDirectAccess -eq $false -and [string]$identityData.sessionSource -ceq ` 'current_logged_in_erp_process' } catch { $bridgeCliIdentityValid = $false } } Add-Check 'bridge_cli_runtime_identity' $bridgeCliIdentityValid ` 'bridge_cli_runtime_identity_invalid' ` '最终签名桥接 CLI 必须实际运行并逐字段证明包版本、协议、本地 ERP 会话来源和禁止数据库直连。' $webViewVersion = if ($runningOnWindows) { Get-WebView2Version } else { $null } $minimumWebViewVersion = [Version]'151.0.4129.50' $parsedWebViewVersion = $null $webViewVersionValid = -not [string]::IsNullOrWhiteSpace($webViewVersion) -and [Version]::TryParse($webViewVersion, [ref]$parsedWebViewVersion) $webViewCompatible = $webViewVersionValid -and $parsedWebViewVersion -ge $minimumWebViewVersion $webViewCode = if ([string]::IsNullOrWhiteSpace($webViewVersion)) { 'webview2_missing' } else { 'webview2_version_unsupported' } $webViewDetail = if (-not $webViewVersionValid) { '未检测到可解析的 Evergreen Runtime 版本。最低要求:151.0.4129.50。' } elseif (-not $webViewCompatible) { "版本:$webViewVersion;最低要求:151.0.4129.50。" } else { "版本:$webViewVersion" } Add-Check 'webview2_runtime' $webViewCompatible $webViewCode $webViewDetail $expectedSpriteWidth = 1536 $expectedSpriteHeight = 1872 $maximumSpriteBytes = 20MB $spriteFull = [IO.Path]::GetFullPath($SpritePath) $licenseFull = [IO.Path]::GetFullPath($SpriteLicenseEvidence) $astrBotComplianceFull = [IO.Path]::GetFullPath($AstrBotComplianceEvidence) $miniMaxComplianceFull = [IO.Path]::GetFullPath($MiniMaxServiceComplianceEvidence) $miniMaxProbeFull = [IO.Path]::GetFullPath($MiniMaxVisionProbeEvidence) $rolloutFull = [IO.Path]::GetFullPath($RolloutPolicyPath) $rolloutSha256 = Get-RegularFileSha256 $rolloutFull 256KB $rolloutFileOk = -not [string]::IsNullOrWhiteSpace($rolloutSha256) Add-Check 'rollout_policy_file' $rolloutFileOk 'rollout_policy_file_invalid' ` '命令发布策略必须是包外、非链接、不超过 256 KB 且可完整哈希的普通文件。' $spriteDimensions = if ((Test-RegularFile $spriteFull $maximumSpriteBytes) -and ([IO.Path]::GetExtension($spriteFull) -eq '.webp')) { Get-WebPDimensions $spriteFull $maximumSpriteBytes } else { $null } $spriteOk = $null -ne $spriteDimensions -and $spriteDimensions.width -eq $expectedSpriteWidth -and $spriteDimensions.height -eq $expectedSpriteHeight $licenseExtension = [IO.Path]::GetExtension($licenseFull).ToLowerInvariant() $licenseOk = (Test-RegularFile $licenseFull 16MB) -and @('.pdf', '.p7s') -contains $licenseExtension $astrBotComplianceExtension = [IO.Path]::GetExtension( $astrBotComplianceFull).ToLowerInvariant() $astrBotComplianceSha256 = if ( @('.pdf', '.p7s') -contains $astrBotComplianceExtension) { Get-RegularFileSha256 $astrBotComplianceFull 16MB } else { $null } $astrBotComplianceOk = -not [string]::IsNullOrWhiteSpace( $astrBotComplianceSha256) $miniMaxComplianceExtension = [IO.Path]::GetExtension( $miniMaxComplianceFull).ToLowerInvariant() $miniMaxComplianceSha256 = if ( @('.pdf', '.p7s') -contains $miniMaxComplianceExtension) { Get-RegularFileSha256 $miniMaxComplianceFull 16MB } else { $null } $miniMaxComplianceOk = -not [string]::IsNullOrWhiteSpace( $miniMaxComplianceSha256) Add-Check 'guga_sprite' $spriteOk 'sprite_invalid' ` '素材必须是包外、结构完整且尺寸为 1536×1872 的普通 WebP 文件。' Add-Check 'guga_commercial_license_evidence' $licenseOk 'sprite_license_missing' '必须提供经人工复核的书面商用授权 PDF 或 P7S 证据。' Add-Check 'astrbot_agpl_eula_compliance_evidence' $astrBotComplianceOk ` 'astrbot_compliance_evidence_missing' ` '必须提供法务复核的 AstrBot AGPL-3.0-or-later、EULA、交付和网络源码方案 PDF 或 P7S。' Add-Check 'minimax_api_service_compliance_evidence' $miniMaxComplianceOk ` 'minimax_service_compliance_evidence_missing' ` '必须提供法务复核的 MiniMax API 服务条款、数据处理、区域、套餐与密钥管理方案 PDF 或 P7S。' $miniMaxProbe = Test-MiniMaxVisionProbeEvidence ` $miniMaxProbeFull ([DateTimeOffset]::UtcNow) Add-Check 'minimax_online_vision_probe_evidence' $miniMaxProbe.passed ` $miniMaxProbe.code $miniMaxProbe.detail $credentialListing = '' if ($runningOnWindows) { $credentialListing = (& "$env:SystemRoot\System32\cmdkey.exe" "/list:$CredentialTarget" 2>$null | Out-String) } $credentialOk = $credentialListing.IndexOf($CredentialTarget, [StringComparison]::OrdinalIgnoreCase) -ge 0 Add-Check 'astrbot_credential' $credentialOk 'astrbot_credential_missing' 'AstrBot Key 必须位于 Windows 通用凭据中。' Add-Check 'desktop_secret_environment' ` (-not $env:LSERP_ASTRBOT_API_KEY -and -not $env:MINIMAX_API_KEY) ` 'desktop_secret_environment_forbidden' '桌宠进程环境不得包含 AstrBot 或 MiniMax Key。' $uri = $null $uriOk = [Uri]::TryCreate($AstrBotBaseUrl, [UriKind]::Absolute, [ref]$uri) if ($uriOk) { $uriOk = $uri.Scheme -in @('http', 'https') -and $uri.IsLoopback -and -not $uri.UserInfo -and -not $uri.Query -and -not $uri.Fragment } Add-Check 'astrbot_transport' $uriOk 'astrbot_loopback_required' ` '当前商用传输只支持同机 loopback AstrBot;远程模式必须先部署双向设备身份 Agent Gateway。' $astrBotReachable = $uriOk -and (Test-TcpEndpoint $uri) Add-Check 'astrbot_reachable' $astrBotReachable 'astrbot_unreachable' '只检查 TCP 连通性,不发送 Key。' $astrbotRuntime = if ($astrBotReachable) { Get-AstrBotRuntimeContract $uri } else { [ordered]@{ passed = $false runtimeVersion = $null codeVersion = $null detail = 'AstrBot 不可达,无法验证运行时与磁盘代码版本。' } } Add-Check 'astrbot_runtime_contract' $astrbotRuntime.passed ` 'astrbot_runtime_contract_mismatch' $astrbotRuntime.detail $bridgeFiles = @() if ([IO.Directory]::Exists($BridgeDiscoveryDirectory)) { $bridgeFiles = @(Get-ChildItem -LiteralPath $BridgeDiscoveryDirectory -Filter 'agentbridge-*.json' -File -ErrorAction SilentlyContinue) } $bridgeHealthy = $false $rolloutAttested = $false $sessionScopeAttested = $false if ($runningOnWindows -and $legacyCliSignature -and $bridgeFiles.Count -gt 0) { try { $firstSession = Invoke-VerifiedBridgeRead 'context' $health = Invoke-VerifiedBridgeRead 'health' $lastSession = Invoke-VerifiedBridgeRead 'context' $bridgeHealthy = $null -ne $health -and (Test-ExactProperties $health @( 'status', 'protocolVersion', 'serverTimeUtc', 'commandCount', 'enabledCommandCount', 'operationalPolicy', 'rolloutPolicy', 'workflowUat')) -and [string]$health.status -ceq 'ready' -and [string]$health.protocolVersion -ceq '1.0' $sessionScopeAttested = (Test-ExpectedSessionContext $firstSession) -and (Test-ExpectedSessionContext $lastSession) -and [string]$firstSession.userId -ceq [string]$lastSession.userId -and [string]$firstSession.userName -ceq [string]$lastSession.userName -and [string]$firstSession.accountBook -ceq [string]$lastSession.accountBook -and [string]$firstSession.subSystemId -ceq [string]$lastSession.subSystemId -and [string]$firstSession.databaseScopeFingerprint -ceq [string]$lastSession.databaseScopeFingerprint -and [bool]$firstSession.isAdministrator -eq [bool]$lastSession.isAdministrator if ($bridgeHealthy) { try { $runtimeRollout = $health.rolloutPolicy $expectedRolloutProperties = @( 'configured', 'failClosed', 'customerId', 'databaseScopeFingerprint', 'sourceSha256', 'defaultAction', 'ruleCount' ) $actualRolloutProperties = @( $runtimeRollout.PSObject.Properties | ForEach-Object { $_.Name } ) $rolloutShapeValid = $actualRolloutProperties.Count -eq $expectedRolloutProperties.Count foreach ($name in $expectedRolloutProperties) { $rolloutShapeValid = $rolloutShapeValid -and ($actualRolloutProperties -ccontains $name) } $runtimeRuleCount = -1 $ruleCountValid = [int]::TryParse( [string]$runtimeRollout.ruleCount, [ref]$runtimeRuleCount) -and $runtimeRuleCount -ge 0 -and $runtimeRuleCount -le 128 $rolloutAttested = $rolloutShapeValid -and $rolloutFileOk -and $runtimeRollout.configured -eq $true -and $runtimeRollout.failClosed -eq $true -and [string]$runtimeRollout.customerId -ceq $RolloutCustomerId -and [string]$runtimeRollout.databaseScopeFingerprint -ceq $ExpectedDatabaseScopeFingerprint -and [string]$runtimeRollout.sourceSha256 -ceq $rolloutSha256 -and [string]$runtimeRollout.defaultAction -ceq 'deny' -and $ruleCountValid } catch { $rolloutAttested = $false } } } catch { $bridgeHealthy = $false } } Add-Check 'erp_agent_bridge' $bridgeHealthy 'erp_bridge_unhealthy' ` '必须由最终签名 CLI 实际连接 -ErpProcessId 指定的当前 ERP 命令桥。' Add-Check 'erp_rollout_policy' $rolloutAttested 'erp_rollout_policy_mismatch' ` '目标 ERP 必须证明已加载与现场文件 SHA-256、客户部署标识和数据库作用域一致的默认拒绝发布策略。' Add-Check 'erp_session_scope' $sessionScopeAttested ` 'erp_session_scope_mismatch' ` '目标 ERP 在健康检查前后都必须逐字匹配预期用户、账套、子系统和数据库作用域;报告不保存这些原始标识。' $astrbotPluginRoot = Join-Path $root 'AstrBotPlugin' $expectedAstrBotPluginFiles = @( 'README.md', '__init__.py', '_conf_schema.json', 'astrbot-contract.json', 'astrbot_contract.py', 'attachment_extract.py', 'attachment_provenance.py', 'attachment_sandbox.py', 'attachment_worker.py', 'bridge_protocol.py', 'main.py', 'metadata.yaml', 'pdf_render_sandbox.py', 'pdf_render_worker.py', 'pdf_vision.py', 'plan_chain.py', 'prompt.py', 'purchase_tabular_binding.py', 'purchase_vision_binding.py', 'requirements.txt', 'session_auth.py', 'tools.py', 'verify_astrbot_contract.py', 'verify_minimax_vlm_contract.py', 'vision.py' ) $astrbotPluginLayoutOk = $false try { $actualAstrBotPluginPaths = @( [IO.Directory]::GetFiles( $astrbotPluginRoot, '*', [IO.SearchOption]::TopDirectoryOnly) ) $actualAstrBotPluginFiles = @( $actualAstrBotPluginPaths | ForEach-Object { [IO.Path]::GetFileName($_) } ) $astrbotPluginLayoutOk = @([IO.Directory]::GetDirectories( $astrbotPluginRoot, '*', [IO.SearchOption]::TopDirectoryOnly)).Count -eq 0 -and $actualAstrBotPluginFiles.Count -eq $expectedAstrBotPluginFiles.Count foreach ($name in $expectedAstrBotPluginFiles) { $astrbotPluginLayoutOk = $astrbotPluginLayoutOk -and ($actualAstrBotPluginFiles -ccontains $name) } foreach ($path in $actualAstrBotPluginPaths) { $astrbotPluginLayoutOk = $astrbotPluginLayoutOk -and (Test-RegularFile $path 2MB) } } catch { $astrbotPluginLayoutOk = $false } Add-Check 'astrbot_plugin_layout' $astrbotPluginLayoutOk ` 'astrbot_plugin_layout_invalid' ` 'AstrBot 插件目录只能包含受审的顶层白名单源码,禁止夹带 data、缓存、凭据或链接。' $metadataPath = Join-Path $astrbotPluginRoot 'metadata.yaml' $astrbotContractPath = Join-Path $root 'AstrBotPlugin\astrbot-contract.json' $astrbotGuardPath = Join-Path $root 'AstrBotPlugin\astrbot_contract.py' $astrbotMainPath = Join-Path $root 'AstrBotPlugin\main.py' $metadataOk = $false if ((Test-RegularFile $metadataPath 64KB) -and (Test-RegularFile $astrbotContractPath 64KB) -and (Test-RegularFile $astrbotGuardPath 64KB) -and (Test-RegularFile $astrbotMainPath 512KB)) { try { $strictUtf8 = New-Object Text.UTF8Encoding($false, $true) $metadata = [IO.File]::ReadAllText($metadataPath, $strictUtf8) $contract = [IO.File]::ReadAllText( $astrbotContractPath, $strictUtf8) | ConvertFrom-Json $guard = [IO.File]::ReadAllText($astrbotGuardPath, $strictUtf8) $main = [IO.File]::ReadAllText($astrbotMainPath, $strictUtf8) $criticalHashProperties = @( $contract.criticalSourceSha256.PSObject.Properties) $criticalHashNames = @( 'astrbot/__init__.py', 'astrbot/api/__init__.py', 'astrbot/api/event/__init__.py', 'astrbot/api/message_components.py', 'astrbot/api/provider/__init__.py', 'astrbot/api/star/__init__.py', 'astrbot/core/agent/run_context.py', 'astrbot/core/agent/tool.py', 'astrbot/core/astr_agent_context.py', 'astrbot/core/message/components.py', 'astrbot/core/provider/entities.py', 'astrbot/core/star/base.py', 'astrbot/core/star/context.py', 'astrbot/core/star/star_manager.py', 'astrbot/dashboard/api/stats.py', 'astrbot/dashboard/responses.py', 'astrbot/dashboard/services/stat_service.py' ) $criticalHashesValid = $criticalHashProperties.Count -eq ` $criticalHashNames.Count foreach ($name in $criticalHashNames) { $property = $contract.criticalSourceSha256.PSObject.Properties[$name] $criticalHashesValid = $criticalHashesValid -and $null -ne $property -and ([string]$property.Value) -cmatch '^[a-f0-9]{64}$' } $metadataOk = $metadata.Contains('version: 0.4.0') -and $metadata.Contains('astrbot_version: "==4.27.2"') -and (Test-ExactProperties $contract @( 'schemaVersion', 'repository', 'tag', 'commit', 'runtimeVersion', 'versionSpecifier', 'pluginVersion', 'license', 'licenseSha256', 'eulaSha256', 'projectMetadataSha256', 'criticalSourceSha256')) -and $contract.schemaVersion -ceq '1.1' -and $contract.repository -ceq ` 'https://github.com/AstrBotDevs/AstrBot.git' -and $contract.tag -ceq 'v4.27.2' -and $contract.commit -ceq ` 'ad4fbfa90ca0c4ac2b30b3250e34dbf8fe7babbf' -and $contract.runtimeVersion -ceq '4.27.2' -and $contract.versionSpecifier -ceq '==4.27.2' -and $contract.pluginVersion -ceq '0.4.0' -and $contract.license -ceq 'AGPL-3.0-or-later' -and $contract.licenseSha256 -ceq ` 'ccf7d08f932af3e813848881731113afbb7c80d0fd6d958e8d319002bf344d02' -and $contract.eulaSha256 -ceq ` 'c332de7781e87c67d6d3beda463fa04705075a6bae9e52a252f7c639f6defd80' -and $contract.projectMetadataSha256 -ceq ` 'd61527cc6ccb6163930f2b32f8518e8ca56a4247cd67e883adfc68a19dd233aa' -and $criticalHashesValid -and $guard.Contains('SUPPORTED_ASTRBOT_VERSION: Final = "4.27.2"') -and $guard.Contains('SUPPORTED_ASTRBOT_SOURCE_SHA256: Final') -and $guard.Contains('critical_source_hash_mismatch') -and $guard.Contains('astrbot_runtime_contract_mismatch') -and $main.Contains('assert_supported_astrbot_runtime()') } catch { $metadataOk = $false } } Add-Check 'astrbot_plugin_contract' $metadataOk 'astrbot_plugin_contract_invalid' ` '插件、运行时守卫和来源证据必须精确锁定 AstrBot 4.27.2。' $visionPath = Join-Path $root 'AstrBotPlugin\vision.py' $visionConfigPath = Join-Path $root 'AstrBotPlugin\_conf_schema.json' $visionProbePath = Join-Path $root 'AstrBotPlugin\verify_minimax_vlm_contract.py' $miniMaxDirectOk = $false if ((Test-RegularFile $visionPath 64KB) -and (Test-RegularFile $visionConfigPath 32KB) -and (Test-RegularFile $visionProbePath 32KB) -and -not (Test-Path -LiteralPath (Join-Path $root 'MmxRuntime'))) { try { $visionText = Get-Content -LiteralPath $visionPath -Raw -Encoding UTF8 $visionConfigText = Get-Content -LiteralPath $visionConfigPath -Raw -Encoding UTF8 $visionProbeText = Get-Content -LiteralPath $visionProbePath -Raw -Encoding UTF8 $miniMaxDirectOk = $visionText.Contains('https://api.minimax.io/v1/coding_plan/vlm') -and $visionText.Contains('https://api.minimaxi.com/v1/coding_plan/vlm') -and $visionText.Contains('MINIMAX_VLM_CONTRACT_VERSION = "0.0.4"') -and $visionText.Contains('MINIMAX_VLM_CONTRACT_COMMIT = "fbac3b3e56922a1249e00eebe07d9ee68f4768dc"') -and $visionText.Contains('fbac3b3e56922a1249e00eebe07d9ee68f4768dc') -and $visionText.Contains('08d4116a20e8a652ceb9e2b6f58b1e7cdfe464b14baff05977e08b4b05b66be3') -and $visionText.Contains('1dea28d6ba4ee46ba516d7eeedd325a5a102410bb7abb074fc4b0a8a66571864') -and $visionText.Contains('MM-API-Source') -and $visionText.Contains('"MM-API-Source": MINIMAX_API_SOURCE') -and $visionText.Contains('MINIMAX_API_SOURCE = "Minimax-MCP"') -and $visionText.Contains('urllib.request.ProxyHandler({})') -and $visionText.Contains('_NoRedirectHandler()') -and $visionText.Contains('MINIMAX_API_KEY') -and $visionText.Contains('assert_minimax_vision_runtime') -and -not $visionText.Contains('create_subprocess_exec') -and -not $visionText.Contains('mmx-cli') -and $visionConfigText.Contains('"minimax_vision_enabled"') -and $visionConfigText.Contains('"minimax_api_region"') -and -not $visionConfigText.Contains('"mmx_executable"') -and $visionProbeText.Contains('def synthetic_probe_png()') -and $visionProbeText.Contains('SYNTHETIC_PROBE_SHA256 = (') -and $visionProbeText.Contains( 'd37476a5273821c12ee4a72b512dc152db5729055b6febb8603985f86243abda') -and $visionProbeText.Contains('if digest != SYNTHETIC_PROBE_SHA256:') -and $visionProbeText.Contains('vision_probe_source_contract_invalid') -and $visionProbeText.Contains('describe_business_image(') -and $visionProbeText.Contains('os.O_EXCL') -and $visionProbeText.Contains('"syntheticSourceSha256"') -and -not $visionProbeText.Contains('image_source') } catch { $miniMaxDirectOk = $false } } Add-Check 'minimax_direct_https_vlm' $miniMaxDirectOk ` 'minimax_direct_https_contract_invalid' ` '必须由 AstrBot 服务端使用固定区域、禁代理、禁重定向的 HTTPS VLM,并提供不含客户数据的合成图片在线探针;交付包不得包含 mmx-cli 或 Node 运行时。' $pdfRenderSandboxPath = Join-Path $root 'AstrBotPlugin\pdf_render_sandbox.py' $pdfRenderWorkerPath = Join-Path $root 'AstrBotPlugin\pdf_render_worker.py' $pdfVisionPath = Join-Path $root 'AstrBotPlugin\pdf_vision.py' $purchaseTabularPath = Join-Path $root ` 'AstrBotPlugin\purchase_tabular_binding.py' $attachmentExtractContractPath = Join-Path $root ` 'AstrBotPlugin\attachment_extract.py' $attachmentProvenanceContractPath = Join-Path $root ` 'AstrBotPlugin\attachment_provenance.py' $sourceDocumentSchemaPath = Join-Path $root ` 'Deployment\SqlServer\001_agent_business_idempotency.sql' $purchaseWriteContractPath = Join-Path $root ` 'Deployment\customer-profiles\lserp-ai.workflow-write.purchase.compat100.draft.sql' $purchaseWireContractPath = Join-Path $root ` 'Contracts\erp-agent-wire-contract-v1.json' $requirementsPath = Join-Path $root 'AstrBotPlugin\requirements.txt' $pdfiumWheelName = 'pypdfium2-5.12.1-py3-none-win_amd64.whl' $pdfiumWheelPath = Join-Path $root ('PythonWheels\' + $pdfiumWheelName) $pdfiumWheelSha256 = ` '9609be73a6701a68f29dffe0335f7a2e4b3ba581542ed65d35d49f761a4600ca' $pdfInvoicePipelineOk = $false $pdfiumWheelStream = $null $pdfiumWheelArchive = $null try { $pythonWheelsPath = Join-Path $root 'PythonWheels' if (-not [IO.Directory]::Exists($pythonWheelsPath)) { throw 'pdfium_wheel_directory_missing' } $pdfiumWheels = @([IO.Directory]::GetFiles( $pythonWheelsPath, 'pypdfium2-*.whl', [IO.SearchOption]::TopDirectoryOnly)) if ($pdfiumWheels.Count -ne 1 -or [IO.Path]::GetFileName($pdfiumWheels[0]) -cne $pdfiumWheelName -or (Get-RegularFileSha256 $pdfiumWheelPath 32MB) -cne $pdfiumWheelSha256) { throw 'pdfium_wheel_identity_invalid' } foreach ($sourcePath in @( $pdfRenderSandboxPath, $pdfRenderWorkerPath, $pdfVisionPath, $purchaseTabularPath, $attachmentExtractContractPath, $attachmentProvenanceContractPath, $sourceDocumentSchemaPath, $purchaseWriteContractPath, $purchaseWireContractPath, $requirementsPath)) { if (-not (Test-RegularFile $sourcePath 512KB)) { throw 'pdf_pipeline_source_missing' } } $strictUtf8 = New-Object Text.UTF8Encoding($false, $true) $requirementsText = [IO.File]::ReadAllText( $requirementsPath, $strictUtf8) $pdfRenderSandboxText = [IO.File]::ReadAllText( $pdfRenderSandboxPath, $strictUtf8) $pdfRenderWorkerText = [IO.File]::ReadAllText( $pdfRenderWorkerPath, $strictUtf8) $pdfVisionText = [IO.File]::ReadAllText($pdfVisionPath, $strictUtf8) $purchaseTabularText = [IO.File]::ReadAllText( $purchaseTabularPath, $strictUtf8) $attachmentMainText = [IO.File]::ReadAllText( $astrbotMainPath, $strictUtf8) $attachmentExtractContractText = [IO.File]::ReadAllText( $attachmentExtractContractPath, $strictUtf8) $attachmentProvenanceContractText = [IO.File]::ReadAllText( $attachmentProvenanceContractPath, $strictUtf8) $sourceDocumentSchemaText = [IO.File]::ReadAllText( $sourceDocumentSchemaPath, $strictUtf8) $purchaseWriteContractText = [IO.File]::ReadAllText( $purchaseWriteContractPath, $strictUtf8) $purchaseWireContractText = [IO.File]::ReadAllText( $purchaseWireContractPath, $strictUtf8) if (-not $requirementsText.Contains('pypdfium2==5.12.1 \') -or -not $requirementsText.Contains( '--hash=sha256:9609be73a6701a68f29dffe0335f7a2e4b3ba581542ed65d35d49f761a4600ca') -or -not $pdfRenderSandboxText.Contains('MAX_PDF_VISION_PAGES = 3') -or -not $pdfRenderSandboxText.Contains('asyncio.create_subprocess_exec(') -or -not $pdfRenderSandboxText.Contains('"-I",') -or -not $pdfRenderSandboxText.Contains('"-B",') -or -not $pdfRenderSandboxText.Contains('_validate_rgb_png(') -or -not $pdfRenderWorkerText.Contains('import pypdfium2 as pdfium') -or -not $pdfRenderWorkerText.Contains( 'sys.addaudithook(_deny_unsafe_runtime_operations)') -or -not $pdfRenderWorkerText.Contains('may_draw_forms=False') -or -not $pdfRenderWorkerText.Contains( 'if page_count > limits["maximumPages"]:') -or -not $pdfRenderWorkerText.Contains( 'source_bytes = _read_source_snapshot(') -or -not $pdfVisionText.Contains( 'PDF_VISION_PIPELINE = "pdfium_minimax_pages_v1"') -or -not $pdfVisionText.Contains('describe_business_image_bytes(') -or -not $pdfVisionText.Contains('merge_purchase_vision_documents(') -or -not $pdfVisionText.Contains('project_pdf_vision_content(content)') -or -not $purchaseTabularText.Contains( 'return project_pdf_vision_content(envelope["content"])') -or -not $attachmentMainText.Contains('describe_business_pdf(') -or -not $attachmentMainText.Contains( 'maximum_pages=remaining_pages') -or -not $attachmentExtractContractText.Contains( 'PREPROCESS_CONTRACT_PDF = "pdfium_minimax_pages_v1"') -or -not $attachmentExtractContractText.Contains( 'result["preprocessContract"] = _preprocess_contract(') -or -not $attachmentExtractContractText.Contains( 'content.get("pipeline") != PREPROCESS_CONTRACT_PDF') -or -not $attachmentProvenanceContractText.Contains( '"preprocessContract": preprocess_contract') -or -not $attachmentProvenanceContractText.Contains( 'expected["preprocessContract"] != preprocess_contract') -or -not $sourceDocumentSchemaText.Contains( 'preprocess_contract VARCHAR(64) NOT NULL') -or -not $sourceDocumentSchemaText.Contains( "'pdfium_minimax_pages_v1'") -or -not $purchaseWriteContractText.Contains( '/source_documents[@version="3"]') -or -not $purchaseWriteContractText.Contains( "'(@preprocess_contract)[1]'") -or -not $purchaseWriteContractText.Contains( 'source_sha256, extraction_sha256, preprocess_contract, size_bytes') -or -not $purchaseWireContractText.Contains( '"commandVersion": "1.4"')) { throw 'pdf_pipeline_contract_invalid' } Add-Type -AssemblyName System.IO.Compression -ErrorAction Stop try { Add-Type -AssemblyName System.IO.Compression.FileSystem ` -ErrorAction Stop } catch { if ($null -eq ('System.IO.Compression.ZipArchive' -as [type])) { throw } } $pdfiumWheelStream = [IO.File]::Open( $pdfiumWheelPath, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) $pdfiumWheelArchive = [IO.Compression.ZipArchive]::new( $pdfiumWheelStream, [IO.Compression.ZipArchiveMode]::Read, $true) if ($pdfiumWheelArchive.Entries.Count -lt 1 -or $pdfiumWheelArchive.Entries.Count -gt 256) { throw 'pdfium_wheel_layout_invalid' } $wheelEntries = New-Object ` 'System.Collections.Generic.HashSet[string]' ` ([StringComparer]::Ordinal) [long]$wheelUncompressedBytes = 0 foreach ($entry in $pdfiumWheelArchive.Entries) { if (-not $wheelEntries.Add([string]$entry.FullName) -or $entry.Length -lt 0 -or $entry.Length -gt 64MB -or $wheelUncompressedBytes -gt 128MB - $entry.Length) { throw 'pdfium_wheel_layout_invalid' } $wheelUncompressedBytes += $entry.Length } foreach ($requiredWheelEntry in @( 'pypdfium2_raw/pdfium.dll', 'pypdfium2-5.12.1.dist-info/licenses/LICENSES/Apache-2.0.txt', 'pypdfium2-5.12.1.dist-info/licenses/LICENSES/BSD-3-Clause.txt', 'pypdfium2-5.12.1.dist-info/licenses/LICENSES/CC-BY-4.0.txt', 'pypdfium2-5.12.1.dist-info/licenses/data/windows_x64/BUILD_LICENSES/pdfium.txt', 'pypdfium2-5.12.1.dist-info/licenses/data/windows_x64/BUILD_LICENSES/pdfium-binaries.txt')) { if (-not $wheelEntries.Contains($requiredWheelEntry)) { throw 'pdfium_wheel_license_or_runtime_missing' } } $pdfInvoicePipelineOk = $true } catch { $pdfInvoicePipelineOk = $false } finally { if ($null -ne $pdfiumWheelArchive) { $pdfiumWheelArchive.Dispose() } if ($null -ne $pdfiumWheelStream) { $pdfiumWheelStream.Dispose() } } Add-Check 'pdf_invoice_pipeline' $pdfInvoicePipelineOk ` 'pdf_invoice_pipeline_invalid' ` '电子 PDF 发票必须使用最多三页的隔离 PDFium 渲染、逐页 MiniMax 严格识别和原文件摘要绑定,并只携带精确哈希且内含许可证的 Windows x64 wheel。' $attachmentSnapshotOk = $false $attachmentMainPath = Join-Path $root 'AstrBotPlugin\main.py' $attachmentExtractPath = Join-Path $root 'AstrBotPlugin\attachment_extract.py' $attachmentSandboxPath = Join-Path $root 'AstrBotPlugin\attachment_sandbox.py' $attachmentWorkerPath = Join-Path $root 'AstrBotPlugin\attachment_worker.py' if ((Test-RegularFile $attachmentMainPath 128KB) -and (Test-RegularFile $attachmentExtractPath 128KB) -and (Test-RegularFile $attachmentSandboxPath 128KB) -and (Test-RegularFile $attachmentWorkerPath 64KB) -and (Test-RegularFile $visionPath 64KB)) { try { $attachmentMainText = Get-Content -LiteralPath $attachmentMainPath ` -Raw -Encoding UTF8 $attachmentExtractText = Get-Content -LiteralPath $attachmentExtractPath ` -Raw -Encoding UTF8 $attachmentSandboxText = Get-Content -LiteralPath $attachmentSandboxPath ` -Raw -Encoding UTF8 $attachmentWorkerText = Get-Content -LiteralPath $attachmentWorkerPath ` -Raw -Encoding UTF8 $attachmentSnapshotOk = ([Text.RegularExpressions.Regex]::Matches( $attachmentMainText, 'expected_sha256\s*=\s*str\(\s*receipt_before\["sha256"\]\s*\)').Count -eq 3) -and ([Text.RegularExpressions.Regex]::Matches( $attachmentMainText, 'expected_size_bytes\s*=\s*int\(\s*receipt_before\["sizeBytes"\]\s*\)').Count -eq 3) -and $attachmentExtractText.Contains('_read_stable_source(') -and $attachmentExtractText.Contains('_verify_expected_source(') -and $attachmentExtractText.Contains('io.BytesIO(source_bytes)') -and $attachmentSandboxText.Contains('"expectedSource"') -and $attachmentWorkerText.Contains('"expectedSource"') -and $visionText.Contains('_read_validated_image(') -and $visionText.Contains( 'hashlib.sha256(image_bytes).hexdigest() != expected_sha256') -and $visionText.Contains('attachment_changed_during_preprocess') } catch { $attachmentSnapshotOk = $false } } Add-Check 'attachment_snapshot_binding' $attachmentSnapshotOk ` 'attachment_snapshot_binding_invalid' ` '视觉与文档预处理必须把实际读取的单一字节快照绑定到识别前来源 SHA-256 和大小。' $wireContractPath = Join-Path $root 'Contracts\erp-agent-wire-contract-v1.json' $wireContractOk = $false if (Test-RegularFile $wireContractPath 256KB) { try { $strictUtf8 = New-Object Text.UTF8Encoding($false, $true) $wireContract = [IO.File]::ReadAllText( $wireContractPath, $strictUtf8) | ConvertFrom-Json $expectedPlanFields = @( 'planId', 'commandName', 'commandVersion', 'moduleCode', 'risk', 'createdAtUtc', 'expiresAtUtc', 'valid', 'executionAllowed', 'inputFingerprint', 'outcomeCode', 'title', 'preview', 'data', 'warnings' ) $actualPlanFields = @($wireContract.planProjectionFields) $fieldsValid = $actualPlanFields.Count -eq $expectedPlanFields.Count for ($index = 0; $index -lt $expectedPlanFields.Count; $index++) { $fieldsValid = $fieldsValid -and [string]$actualPlanFields[$index] -ceq $expectedPlanFields[$index] } $expectedScenarios = @( 'purchase_resolve_to_create', 'leave_resolve_to_create', 'leave_submit_followup', 'module_diagnose_read_only', 'module_trace_initialization', 'dynamic_module_resolve_to_create', 'dynamic_module_resolve_to_update', 'module_navigate' ) $scenarios = @($wireContract.scenarios) $scenarioNames = @($scenarios | ForEach-Object { [string]$_.name }) $scenariosValid = $scenarios.Count -eq $expectedScenarios.Count foreach ($name in $expectedScenarios) { $scenariosValid = $scenariosValid -and ($scenarioNames -ccontains $name) } foreach ($scenario in $scenarios) { $plan = $scenario.plan $createdAt = [DateTimeOffset]::MinValue $expiresAt = [DateTimeOffset]::MinValue $createdValid = [DateTimeOffset]::TryParse( [string]$plan.createdAtUtc, [ref]$createdAt) $expiresValid = [DateTimeOffset]::TryParse( [string]$plan.expiresAtUtc, [ref]$expiresAt) $risk = [string]$plan.risk $executionExpected = $plan.valid -eq $true -and @('navigate', 'write', 'critical') -ccontains $risk $scenariosValid = $scenariosValid -and (Test-ExactProperties $scenario @( 'name', 'requestedCommand', 'autoFollowedFrom', 'plan')) -and (Test-ExactProperties $plan $expectedPlanFields) -and ([string]$plan.planId -cmatch '^[A-Fa-f0-9]{32}$') -and ([string]$plan.commandName -cmatch '^[A-Za-z0-9_.:-]{1,128}$') -and ([string]$plan.commandVersion -cmatch '^[0-9]+(?:\.[0-9]+){1,3}$') -and ([string]$plan.moduleCode -cmatch '^[A-Za-z0-9_.:-]{1,128}$') -and (@('read', 'navigate', 'draft', 'write', 'critical') -ccontains $risk) -and ([string]$plan.inputFingerprint -cmatch '^[a-f0-9]{64}$') -and ([string]$plan.outcomeCode -cmatch '^[A-Za-z0-9_.:-]{1,128}$') -and ($plan.valid -is [bool]) -and ($plan.executionAllowed -is [bool]) -and ($plan.executionAllowed -eq $executionExpected) -and $createdValid -and $expiresValid -and $expiresAt -gt $createdAt -and ($expiresAt - $createdAt) -le [TimeSpan]::FromMinutes(15) -and $null -ne $plan.data -and @($plan.warnings).Count -le 64 } $wireContractOk = (Test-ExactProperties $wireContract @( 'schemaVersion', 'protocolVersion', 'requestSessionScope', 'planProjectionFields', 'trustedBridgeCorrelationId', 'scenarios')) -and $wireContract.schemaVersion -ceq '1.1' -and $wireContract.protocolVersion -ceq '1.0' -and $wireContract.requestSessionScope.field -ceq 'sessionScopeToken' -and $wireContract.requestSessionScope.tokenVersion -ceq 'v3' -and $wireContract.requestSessionScope.format -ceq '32-lowercase-hex' -and @($wireContract.requestSessionScope.requiredMethods).Count -eq 3 -and @($wireContract.requestSessionScope.bootstrapOptionalMethods).Count -eq 2 -and @($wireContract.requestSessionScope.boundFields).Count -eq 6 -and $wireContract.requestSessionScope.serverAuthoritativeRecheck -eq $true -and ([string]$wireContract.trustedBridgeCorrelationId -cmatch '^[A-Za-z0-9_.:-]{8,128}$') -and $fieldsValid -and $scenariosValid } catch { $wireContractOk = $false } } Add-Check 'erp_wire_contract' $wireContractOk ` 'erp_wire_contract_invalid' ` '交付包必须携带版本化、精确字段且覆盖采购、请假和模块诊断的 ERP Agent 线协议。' $passed = ($checks | Where-Object { -not $_.passed }).Count -eq 0 $spriteHash = if ($spriteOk) { $spriteDimensions.sha256 } else { $null } $licenseHash = if ($licenseOk) { (Get-FileHash -LiteralPath $licenseFull -Algorithm SHA256).Hash.ToLowerInvariant() } else { $null } $nextStep = if ($passed) { '继续执行 CUSTOMER_ACCEPTANCE.md 的客户 ERP/SQL Server 三条真实闭环;本报告本身不启用写命令。' } else { '修复失败门禁后生成新报告;不得启用客户写命令。' } $report = [ordered]@{ schemaVersion = '1.7' generatedAtUtc = [DateTime]::UtcNow.ToString('o') passed = $passed packageManifestVerified = $manifestPassed packageSha256 = $packageArchiveBinding.sha256 packageSourceCommit = $packageCommit legacyBuildEvidenceSha256 = $legacyEvidence.evidenceSha256 rolloutPolicySha256 = $rolloutSha256 rolloutCustomerId = $RolloutCustomerId spriteSha256 = $spriteHash spriteLicenseEvidenceSha256 = $licenseHash astrBotComplianceEvidenceSha256 = $astrBotComplianceSha256 miniMaxServiceComplianceEvidenceSha256 = $miniMaxComplianceSha256 miniMaxIntegrationMode = 'direct_https_vlm' miniMaxVisionProbeEvidenceSha256 = $miniMaxProbe.sha256 miniMaxVisionProbeObservedAtUtc = $miniMaxProbe.observedAtUtc miniMaxVisionProbeRegion = $miniMaxProbe.region miniMaxVisionProbeContractVersion = $miniMaxProbe.contractVersion checks = $checks nextStep = $nextStep } [IO.Directory]::CreateDirectory([IO.Path]::GetFullPath($ReportDirectory)) | Out-Null $reportPath = Join-Path ([IO.Path]::GetFullPath($ReportDirectory)) ` ("commercial-preflight-{0}.json" -f ([DateTime]::UtcNow.ToString('yyyyMMddTHHmmssfffZ'))) $utf8 = New-Object Text.UTF8Encoding($false, $true) $bytes = $utf8.GetBytes(($report | ConvertTo-Json -Depth 10)) $stream = [IO.File]::Open($reportPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) try { $stream.Write($bytes, 0, $bytes.Length); $stream.Flush() } finally { $stream.Dispose() } [ordered]@{ passed = $passed reportPath = $reportPath failedChecks = @($checks | Where-Object { -not $_.passed } | ForEach-Object { $_.name }) } | ConvertTo-Json -Depth 4 if (-not $passed) { exit 6 }