feat: add ERP agent pet bridge and startup guide
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
[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
|
||||
Reference in New Issue
Block a user