Files
lserp_cs_6.0/插件库/Lskj.AgentBridge/Deployment/New-WorkflowWriteIntegrationEvidence.ps1
T
2026-08-14 14:28:28 +08:00

298 lines
15 KiB
PowerShell

[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][ValidateSet('purchase', 'leave')][string]$Workflow,
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_.:-]{1,64}$')][string]$ModuleCode,
[Parameter(Mandatory = $true)][string]$AccountBook,
[Parameter(Mandatory = $true)][string]$SubSystemId,
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Fa-f0-9]{40}$')][string]$SourceCommit,
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Fa-f0-9]{64}$')][string]$PackageSha256,
[Parameter(Mandatory = $true)][string]$RuntimeConfigurationFile,
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$')][string]$RolloutCustomerId,
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_.:-]{8,128}$')][string]$EnvironmentId,
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_.:-]{1,128}$')][string]$TestedBy,
[Parameter(Mandatory = $true)][string]$CasesFile,
[Parameter(Mandatory = $true)][string]$UatAuthorizationFile,
[Parameter(Mandatory = $true)][string]$VerifierCliPath,
[Parameter(Mandatory = $true)][string]$OutputPath
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
function Get-RegularFile([string]$Path, [long]$MaximumBytes, [string]$Label) {
$full = [IO.Path]::GetFullPath($Path)
if (-not [IO.File]::Exists($full)) { throw "$Label file does not exist." }
$info = Get-Item -LiteralPath $full -Force
if ($info.Length -le 0 -or $info.Length -gt $MaximumBytes -or
(($info.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)) {
throw "$Label must be a non-empty ordinary file within the size limit."
}
return $full
}
function Get-Sha256([byte[]]$Bytes) {
$sha = [Security.Cryptography.SHA256]::Create()
try {
return ([BitConverter]::ToString($sha.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant()
}
finally { $sha.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
}
$strictUtf8 = New-Object Text.UTF8Encoding($false, $true)
$runtimePath = Get-RegularFile $RuntimeConfigurationFile 64KB 'Runtime configuration'
$casesPath = Get-RegularFile $CasesFile 2MB 'Cases'
$uatAuthorizationPath = Get-RegularFile `
$UatAuthorizationFile 512KB 'UAT authorization'
$cliPath = Get-RegularFile $VerifierCliPath 64MB 'Verifier CLI'
if ([IO.Path]::GetFileName($cliPath) -ne 'lserp-cli.exe') {
throw 'Verifier CLI filename must be lserp-cli.exe.'
}
$runtimeBytes = [IO.File]::ReadAllBytes($runtimePath)
$runtimeHash = Get-Sha256 $runtimeBytes
$uatAuthorizationHash = Get-Sha256 `
([IO.File]::ReadAllBytes($uatAuthorizationPath))
$cliHash = Get-Sha256 ([IO.File]::ReadAllBytes($cliPath))
$uatOutput = @(& $cliPath 'acceptance' 'verify-uat-authorization' `
'--input' $uatAuthorizationPath '--json' 2>&1)
if ($LASTEXITCODE -ne 0) {
throw 'UAT authorization failed final CLI verification.'
}
try {
$uatVerification = (($uatOutput | ForEach-Object { [string]$_ }) -join `
[Environment]::NewLine) | ConvertFrom-Json
}
catch { throw 'UAT authorization verifier returned invalid JSON.' }
if (-not (Test-ExactProperties $uatVerification @('ok', 'correlationId', 'data')) -or
-not (Test-ExactProperties $uatVerification.data.erpScope @(
'accountBook', 'subSystemId', 'userIdSha256', 'userNameSha256',
'databaseScopeFingerprint', 'isAdministrator')) -or
-not (Test-ExactProperties $uatVerification.data.runtimeCli @(
'fileName', 'version', 'sha256', 'signerThumbprint',
'requiresElevation', 'bridgeOnly', 'databaseDirectAccess',
'sessionSource')) -or
-not (Test-ExactProperties $uatVerification.data.verifierCli @(
'fileName', 'sha256', 'signerThumbprint',
'requiresElevation')) -or
$uatVerification.ok -ne $true -or
$uatVerification.data.packageType -cne 'workflow_write_uat_authorization' -or
$uatVerification.data.schemaVersion -cne '1.2' -or
$uatVerification.data.sourceSha256 -cne $uatAuthorizationHash -or
$uatVerification.data.runtimeConfigurationSha256 -cne $runtimeHash -or
$uatVerification.data.sourceCommit -cne $SourceCommit.ToLowerInvariant() -or
$uatVerification.data.packageSha256 -cne $PackageSha256.ToLowerInvariant() -or
$uatVerification.data.customerId -cne $RolloutCustomerId -or
$uatVerification.data.environmentId -cne $EnvironmentId -or
$uatVerification.data.erpScope.accountBook -cne $AccountBook -or
$uatVerification.data.erpScope.subSystemId -cne $SubSystemId -or
$uatVerification.data.runtimeCli.fileName -cne 'lserp-agent-cli.exe' -or
[string]$uatVerification.data.runtimeCli.version -cnotmatch
'^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}$' -or
[string]$uatVerification.data.runtimeCli.sha256 -cnotmatch
'^[a-f0-9]{64}$' -or
[string]$uatVerification.data.runtimeCli.signerThumbprint -cnotmatch
'^[A-F0-9]{40}$' -or
$uatVerification.data.runtimeCli.requiresElevation -ne $false -or
$uatVerification.data.runtimeCli.bridgeOnly -ne $true -or
$uatVerification.data.runtimeCli.databaseDirectAccess -ne $false -or
$uatVerification.data.runtimeCli.sessionSource -cne
'current_logged_in_erp_process' -or
$uatVerification.data.verifierCli.fileName -cne 'lserp-cli.exe' -or
$uatVerification.data.verifierCli.sha256 -cne $cliHash -or
$uatVerification.data.verifierCli.requiresElevation -ne $true -or
$uatVerification.data.signatureVerified -ne $true -or
$uatVerification.data.uatAuthorized -ne $true -or
$uatVerification.data.productionReady -ne $false) {
throw 'UAT authorization does not bind the final test scope and verifier.'
}
$authorizedWorkflows = @($uatVerification.data.workflows | Where-Object {
[string]$_.workflow -ceq $Workflow -and
[string]$_.moduleCode -ceq $ModuleCode
})
if ($authorizedWorkflows.Count -ne 1) {
throw 'UAT authorization does not contain the exact workflow and module.'
}
$casesText = [IO.File]::ReadAllText($casesPath, $strictUtf8)
$casesHash = Get-Sha256 ($strictUtf8.GetBytes($casesText))
try { $cases = @($casesText | ConvertFrom-Json) }
catch { throw 'Cases file must be a UTF-8 JSON array.' }
if ($cases.Count -le 0 -or $cases.Count -gt 32 -or $casesText.TrimStart()[0] -ne '[') {
throw 'Cases file must contain a JSON array with 1-32 items.'
}
$runtimeCliVersion = [string]$uatVerification.data.runtimeCli.version
$runtimeCliSha256 = [string]$uatVerification.data.runtimeCli.sha256
$runtimeCliSigner = [string]$uatVerification.data.runtimeCli.signerThumbprint
foreach ($case in $cases) {
if ([string]$case.runtimeCliVersion -cne $runtimeCliVersion -or
[string]$case.runtimeCliSha256 -cne $runtimeCliSha256 -or
[string]$case.runtimeCliSignerThumbprint -cne $runtimeCliSigner) {
throw 'A projected case does not bind the authorized runtime CLI.'
}
}
$testedAt = [DateTime]::UtcNow
$content = [ordered]@{
evidenceType = 'workflow_write_integration'
workflow = $Workflow
moduleCode = $ModuleCode
erpScope = [ordered]@{
accountBook = $AccountBook
subSystemId = $SubSystemId
}
sourceCommit = $SourceCommit.ToLowerInvariant()
packageSha256 = $PackageSha256.ToLowerInvariant()
runtimeConfigurationSha256 = $runtimeHash
runtimeCli = [ordered]@{
fileName = 'lserp-agent-cli.exe'
version = $runtimeCliVersion
sha256 = $runtimeCliSha256
signerThumbprint = $runtimeCliSigner
requiresElevation = $false
bridgeOnly = $true
databaseDirectAccess = $false
sessionSource = 'current_logged_in_erp_process'
}
uatAuthorizationSourceSha256 = [string]$uatVerification.data.sourceSha256
uatAuthorizationContentSha256 = [string]$uatVerification.data.contentSha256
uatAuthorizationIdSha256 = [string]$uatVerification.data.authorizationIdSha256
environmentId = $EnvironmentId
testedAtUtc = $testedAt.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
testedBy = $TestedBy
cases = $cases
}
$canonical = $content | ConvertTo-Json -Compress -Depth 12
$contentHash = Get-Sha256 $strictUtf8.GetBytes($canonical)
$envelope = [ordered]@{
schemaVersion = '1.6'
contentSha256 = $contentHash
content = $content
}
$body = $strictUtf8.GetBytes(($envelope | ConvertTo-Json -Depth 12) + [Environment]::NewLine)
$bodyHash = Get-Sha256 $body
$target = [IO.Path]::GetFullPath($OutputPath)
$directory = [IO.Path]::GetDirectoryName($target)
if ([string]::IsNullOrWhiteSpace($directory) -or -not [IO.Directory]::Exists($directory) -or
[IO.File]::Exists($target) -or [IO.Directory]::Exists($target)) {
throw 'Output must be a new file in an existing directory.'
}
$temporary = Join-Path $directory ('.lserp-write-evidence-' + [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() }
$arguments = @(
'adapters', 'verify-write-integration-evidence', '--input', $temporary,
'--workflow', $Workflow, '--module', $ModuleCode,
'--account-book', $AccountBook, '--subsystem', $SubSystemId,
'--runtime-sha256', $runtimeHash,
'--source-commit', $SourceCommit.ToLowerInvariant(),
'--package-sha256', $PackageSha256.ToLowerInvariant()
)
$verificationOutput = @(& $cliPath @arguments 2>&1)
if ($LASTEXITCODE -ne 0) { throw 'Generated write integration evidence failed CLI verification.' }
$verificationText = (($verificationOutput | ForEach-Object { [string]$_ }) -join [Environment]::NewLine)
try { $verification = $verificationText | ConvertFrom-Json }
catch { throw 'Verifier CLI returned invalid JSON.' }
$expectedData = @(
'evidenceType', 'schemaVersion', 'contentSha256', 'workflow', 'moduleCode',
'erpScope', 'sourceCommit', 'packageSha256', 'runtimeConfigurationSha256',
'runtimeCli',
'uatAuthorizationSourceSha256', 'uatAuthorizationContentSha256',
'uatAuthorizationIdSha256',
'environmentId', 'testedAtUtc', 'testedBy', 'caseCount', 'verified',
'registrationReady'
)
if (-not (Test-ExactProperties $verification @('ok', 'correlationId', 'data')) -or
-not (Test-ExactProperties $verification.data $expectedData) -or
-not (Test-ExactProperties $verification.data.erpScope @(
'accountBook', 'subSystemId', 'userIdSha256', 'userNameSha256',
'databaseScopeFingerprint', 'isAdministrator')) -or
-not (Test-ExactProperties $verification.data.runtimeCli @(
'fileName', 'version', 'sha256', 'signerThumbprint',
'requiresElevation', 'bridgeOnly', 'databaseDirectAccess',
'sessionSource')) -or
$verification.ok -ne $true -or $verification.data.verified -ne $true -or
$verification.data.schemaVersion -ne '1.6' -or
$verification.data.registrationReady -ne $false -or
$verification.data.contentSha256 -ne $contentHash -or
$verification.data.workflow -ne $Workflow -or
$verification.data.moduleCode -ne $ModuleCode -or
$verification.data.erpScope.accountBook -ne $AccountBook -or
$verification.data.erpScope.subSystemId -ne $SubSystemId -or
$verification.data.erpScope.userIdSha256 -cne
$uatVerification.data.erpScope.userIdSha256 -or
$verification.data.erpScope.userNameSha256 -cne
$uatVerification.data.erpScope.userNameSha256 -or
$verification.data.erpScope.databaseScopeFingerprint -cne
$uatVerification.data.erpScope.databaseScopeFingerprint -or
$verification.data.erpScope.isAdministrator -ne
$uatVerification.data.erpScope.isAdministrator -or
$verification.data.runtimeConfigurationSha256 -ne $runtimeHash -or
$verification.data.runtimeCli.fileName -cne 'lserp-agent-cli.exe' -or
$verification.data.runtimeCli.version -cne $runtimeCliVersion -or
$verification.data.runtimeCli.sha256 -cne $runtimeCliSha256 -or
$verification.data.runtimeCli.signerThumbprint -cne $runtimeCliSigner -or
$verification.data.runtimeCli.requiresElevation -ne $false -or
$verification.data.runtimeCli.bridgeOnly -ne $true -or
$verification.data.runtimeCli.databaseDirectAccess -ne $false -or
$verification.data.runtimeCli.sessionSource -cne
'current_logged_in_erp_process' -or
$verification.data.uatAuthorizationSourceSha256 -ne
$uatVerification.data.sourceSha256 -or
$verification.data.uatAuthorizationContentSha256 -ne
$uatVerification.data.contentSha256 -or
$verification.data.uatAuthorizationIdSha256 -ne
$uatVerification.data.authorizationIdSha256 -or
$verification.data.sourceCommit -ne $SourceCommit.ToLowerInvariant() -or
$verification.data.packageSha256 -ne $PackageSha256.ToLowerInvariant() -or
[int]$verification.data.caseCount -ne $cases.Count -or
(Get-Sha256 ([IO.File]::ReadAllBytes($temporary))) -ne $bodyHash) {
throw 'Verifier CLI response is not bound to the generated evidence.'
}
if ((Get-Sha256 ([IO.File]::ReadAllBytes($uatAuthorizationPath))) -ne
$uatAuthorizationHash -or
(Get-Sha256 ([IO.File]::ReadAllBytes($runtimePath))) -ne $runtimeHash -or
(Get-Sha256 ([IO.File]::ReadAllBytes($casesPath))) -ne $casesHash -or
(Get-Sha256 ([IO.File]::ReadAllBytes($cliPath))) -ne $cliHash) {
throw 'A locked evidence input changed during final verification.'
}
[IO.File]::Move($temporary, $target)
$published = $true
}
finally {
if (-not $published -and [IO.File]::Exists($temporary)) {
[IO.File]::Delete($temporary)
}
}
[ordered]@{
outputFile = $target
workflow = $Workflow
moduleCode = $ModuleCode
accountBook = $AccountBook
subSystemId = $SubSystemId
sourceCommit = $SourceCommit.ToLowerInvariant()
packageSha256 = $PackageSha256.ToLowerInvariant()
runtimeConfigurationSha256 = $runtimeHash
runtimeCliVersion = $runtimeCliVersion
runtimeCliSha256 = $runtimeCliSha256
runtimeCliSignerThumbprint = $runtimeCliSigner
uatAuthorizationSourceSha256 = [string]$uatVerification.data.sourceSha256
uatAuthorizationContentSha256 = [string]$uatVerification.data.contentSha256
uatAuthorizationIdSha256 = [string]$uatVerification.data.authorizationIdSha256
contentSha256 = $contentHash
caseCount = $cases.Count
testedAtUtc = $testedAt.ToString('o')
nextStep = 'Run lserp-cli adapters verify-write-integration-evidence, then sign the workflow acceptance manifest.'
} | ConvertTo-Json -Depth 5