Files
2026-08-14 14:28:28 +08:00

351 lines
15 KiB
PowerShell

[CmdletBinding()]
param(
[string]$PackageRoot = $PSScriptRoot,
[Parameter(Mandatory = $true)][string]$PackageArchivePath,
[Parameter(Mandatory = $true)]
[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,
[Parameter(Mandatory = $true)][string]$LegacyArtifactRoot,
[Parameter(Mandatory = $true)][string]$RolloutPolicyPath,
[Parameter(Mandatory = $true)]
[ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$')]
[string]$RolloutCustomerId,
[string]$AstrBotBaseUrl = 'http://127.0.0.1:6185',
[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]$BridgeDiscoveryDirectory = "$env:LOCALAPPDATA\Langsu\Lserp\AgentBridge",
[string]$PreflightReportDirectory = "$env:LOCALAPPDATA\Langsu\Lserp\AcceptanceReports",
[ValidatePattern('^[A-Za-z0-9_.-]{1,128}$')]
[string]$CredentialTarget = 'Langsu.Lserp.AstrBot.ApiKey'
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
$expectedAdministrator = $ExpectedIsAdministrator -ieq 'true'
if ($env:LSERP_ASTRBOT_API_KEY) {
throw 'Commercial startup refuses LSERP_ASTRBOT_API_KEY in the process environment; use Windows Credential Manager.'
}
if ($env:MINIMAX_API_KEY) {
throw 'MiniMax credentials must stay in the AstrBot service account, not in the desktop process environment.'
}
foreach ($expectedScopeValue in @(
$ExpectedUserId,
$ExpectedUserName,
$ExpectedAccountBook,
$ExpectedSubSystemId)) {
if ([string]::IsNullOrWhiteSpace($expectedScopeValue) -or
$expectedScopeValue -cne $expectedScopeValue.Trim()) {
throw 'Expected ERP session scope values must be nonblank and trimmed.'
}
foreach ($character in $expectedScopeValue.ToCharArray()) {
if ([char]::IsControl($character)) {
throw 'Expected ERP session scope values must not contain control characters.'
}
}
}
$ExpectedDatabaseScopeFingerprint =
$ExpectedDatabaseScopeFingerprint.ToLowerInvariant()
function Get-ErpSessionScopeToken {
param(
[Parameter(Mandatory = $true)][string]$DatabaseScopeFingerprint,
[Parameter(Mandatory = $true)][string]$UserId,
[Parameter(Mandatory = $true)][string]$UserName,
[Parameter(Mandatory = $true)][string]$AccountBook,
[Parameter(Mandatory = $true)][string]$SubSystemId,
[Parameter(Mandatory = $true)][bool]$IsAdministrator
)
$builder = New-Object Text.StringBuilder
[void]$builder.Append("lserp-pet-session-scope-v3`n")
$administratorText = if ($IsAdministrator) { 'true' } else { 'false' }
foreach ($part in @(
@('databaseScopeFingerprint', $DatabaseScopeFingerprint),
@('userId', $UserId),
@('userName', $UserName),
@('accountBook', $AccountBook),
@('subSystemId', $SubSystemId),
@('isAdministrator', $administratorText))) {
$byteCount = [Text.Encoding]::UTF8.GetByteCount([string]$part[1])
[void]$builder.Append([string]$part[0])
[void]$builder.Append('=')
[void]$builder.Append($byteCount.ToString(
[Globalization.CultureInfo]::InvariantCulture))
[void]$builder.Append(':')
[void]$builder.Append([string]$part[1])
[void]$builder.Append("`n")
}
$algorithm = [Security.Cryptography.SHA256]::Create()
try {
$digest = $algorithm.ComputeHash(
[Text.Encoding]::UTF8.GetBytes($builder.ToString()))
}
finally {
if ($null -ne $algorithm) { $algorithm.Dispose() }
}
return -join @($digest[0..15] | ForEach-Object { $_.ToString('x2') })
}
$expectedSessionScopeToken = Get-ErpSessionScopeToken `
-DatabaseScopeFingerprint $ExpectedDatabaseScopeFingerprint `
-UserId $ExpectedUserId `
-UserName $ExpectedUserName `
-AccountBook $ExpectedAccountBook `
-SubSystemId $ExpectedSubSystemId `
-IsAdministrator $expectedAdministrator
$uri = $null
if (-not [Uri]::TryCreate($AstrBotBaseUrl, [UriKind]::Absolute, [ref]$uri)) {
throw 'AstrBot base URL is invalid.'
}
if ($uri.Scheme -notin @('http', 'https') -or
-not $uri.IsLoopback -or
$uri.UserInfo -or $uri.Query -or $uri.Fragment) {
throw 'Current commercial transport requires same-machine loopback AstrBot without URL credentials, query, or fragment; remote mode requires the future Agent Gateway.'
}
$root = [IO.Path]::GetFullPath($PackageRoot)
$packageArchiveFull = [IO.Path]::GetFullPath($PackageArchivePath)
$hostPath = [IO.Path]::GetFullPath((Join-Path $root 'Host\Lskj.AgentPet.Host.exe'))
$hostCriticalPaths = @(
$hostPath,
[IO.Path]::GetFullPath((Join-Path $root 'Host\Lskj.AgentPet.Host.dll')),
[IO.Path]::GetFullPath((Join-Path $root 'Host\Lskj.AgentPet.Host.Core.dll')),
[IO.Path]::GetFullPath((Join-Path $root 'Host\lserp-agent-cli.exe'))
)
$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)
$legacyArtifactFull = [IO.Path]::GetFullPath($LegacyArtifactRoot)
$rolloutPolicyFull = [IO.Path]::GetFullPath($RolloutPolicyPath)
foreach ($required in @($hostCriticalPaths + @(
$packageArchiveFull,
$spriteFull, $licenseFull, $astrBotComplianceFull,
$miniMaxComplianceFull, $miniMaxProbeFull))) {
if (-not [IO.File]::Exists($required)) {
throw "Required file not found: $required"
}
$item = Get-Item -LiteralPath $required
if ($item.Length -le 0 -or
(($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)) {
throw "Required input must be a non-empty regular file: $required"
}
}
if ([IO.Path]::GetExtension($packageArchiveFull) -cne '.zip' -or
(Get-Item -LiteralPath $packageArchiveFull -Force).Length -gt 4GB) {
throw 'PackageArchivePath must be the final commercial ZIP no larger than 4 GB.'
}
if (-not [IO.File]::Exists($rolloutPolicyFull)) {
throw 'Command rollout policy file was not found.'
}
$rolloutPolicyItem = Get-Item -LiteralPath $rolloutPolicyFull -Force
if ($rolloutPolicyItem.Length -le 0 -or
$rolloutPolicyItem.Length -gt 256KB -or
(($rolloutPolicyItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)) {
throw 'Command rollout policy must be a non-empty regular file no larger than 256 KB.'
}
foreach ($evidence in @(
$licenseFull, $astrBotComplianceFull, $miniMaxComplianceFull)) {
$item = Get-Item -LiteralPath $evidence -Force
$extension = [IO.Path]::GetExtension($evidence).ToLowerInvariant()
if (@('.pdf', '.p7s') -notcontains $extension -or $item.Length -gt 16MB) {
throw 'Commercial third-party compliance evidence must be a reviewed PDF or P7S file no larger than 16 MB.'
}
}
if ([IO.Path]::GetExtension($miniMaxProbeFull) -cne '.json' -or
(Get-Item -LiteralPath $miniMaxProbeFull -Force).Length -gt 64KB) {
throw 'MiniMaxVisionProbeEvidence must be a JSON file no larger than 64 KB.'
}
$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) {
throw 'Desktop host and bridge CLI first-party binaries must use the expected valid Authenticode certificate.'
}
}
$verifyPath = [IO.Path]::GetFullPath((Join-Path $root 'Verify-LserpCommercialPackage.ps1'))
$windowsPowerShell = Join-Path $env:SystemRoot `
'System32\WindowsPowerShell\v1.0\powershell.exe'
if (-not [IO.File]::Exists($verifyPath) -or
-not [IO.File]::Exists($windowsPowerShell)) {
throw 'Commercial preflight verifier or Windows PowerShell 5.1 is missing.'
}
$preflightErpProcess = Get-Process -Id $ErpProcessId -ErrorAction Stop
$preflightErpStartedAtUtc =
$preflightErpProcess.StartTime.ToUniversalTime()
$preflightErpExecutable =
[IO.Path]::GetFullPath($preflightErpProcess.MainModule.FileName)
if ([IO.Path]::GetFileName($preflightErpExecutable) -cne 'Ls_ERP.exe') {
throw 'ErpProcessId must identify the intended Ls_ERP.exe process.'
}
$preflightArguments = @(
'-NoLogo', '-NoProfile', '-File', $verifyPath,
'-PackageRoot', $root,
'-PackageArchivePath', $packageArchiveFull,
'-ExpectedPackageVersion', $ExpectedPackageVersion,
'-SpritePath', $spriteFull,
'-SpriteLicenseEvidence', $licenseFull,
'-AstrBotComplianceEvidence', $astrBotComplianceFull,
'-MiniMaxServiceComplianceEvidence', $miniMaxComplianceFull,
'-MiniMaxVisionProbeEvidence', $miniMaxProbeFull,
'-HostCertificateThumbprint', $expectedHostThumbprint,
'-AstrBotBaseUrl', $uri.AbsoluteUri,
'-CredentialTarget', $CredentialTarget,
'-BridgeDiscoveryDirectory', $BridgeDiscoveryDirectory,
'-LegacyArtifactRoot', $legacyArtifactFull,
'-RolloutPolicyPath', $rolloutPolicyFull,
'-RolloutCustomerId', $RolloutCustomerId,
'-ErpProcessId', [string]$ErpProcessId,
'-ExpectedDatabaseScopeFingerprint',
$ExpectedDatabaseScopeFingerprint,
'-ExpectedUserId', $ExpectedUserId,
'-ExpectedUserName', $ExpectedUserName,
'-ExpectedAccountBook', $ExpectedAccountBook,
'-ExpectedSubSystemId', $ExpectedSubSystemId,
'-ExpectedIsAdministrator', $(if ($expectedAdministrator) {
'true'
} else { 'false' }),
'-ReportDirectory', $PreflightReportDirectory
)
& $windowsPowerShell @preflightArguments | Out-Null
if ($LASTEXITCODE -ne 0) {
throw 'Commercial preflight failed; desktop host will not start.'
}
$credentialListing = (& "$env:SystemRoot\System32\cmdkey.exe" "/list:$CredentialTarget" 2>$null | Out-String)
if ($LASTEXITCODE -ne 0 -or $credentialListing.IndexOf(
$CredentialTarget,
[StringComparison]::OrdinalIgnoreCase) -lt 0) {
throw 'AstrBot credential is missing from Windows Credential Manager.'
}
$discoveryRoot = [IO.Path]::GetFullPath($BridgeDiscoveryDirectory)
if (-not [IO.Directory]::Exists($discoveryRoot)) {
throw 'No running ERP AgentBridge discovery directory was found.'
}
$liveBridges = @()
foreach ($file in @(Get-ChildItem -LiteralPath $discoveryRoot -Filter 'agentbridge-*.json' -File -ErrorAction SilentlyContinue)) {
try {
if ($file.Length -le 0 -or $file.Length -gt 65536 -or
(($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)) {
continue
}
$document = [IO.File]::ReadAllText($file.FullName) | ConvertFrom-Json
$pidText = [string]$document.processId
$pidValue = 0
$bridgeInstanceId = [string]$document.bridgeInstanceId
$expectedPipeName = $null
if (-not [int]::TryParse($pidText, [ref]$pidValue) -or $pidValue -le 0 -or
$file.Name -ine "agentbridge-$pidValue.json" -or
[string]$document.protocolVersion -cne '1.0' -or
-not [Text.RegularExpressions.Regex]::IsMatch(
$bridgeInstanceId, '^[a-f0-9]{32}$')) {
continue
}
$expectedPipeName = "lserp.agent.$pidValue.$bridgeInstanceId"
if ([string]$document.pipeName -cne $expectedPipeName) {
continue
}
$startedAt = [DateTimeOffset]::MinValue
$startedAtText = [string]$document.startedAtUtc
if (-not [Text.RegularExpressions.Regex]::IsMatch(
$startedAtText,
'(?:Z|[+-][0-9]{2}:[0-9]{2})$') -or
-not [DateTimeOffset]::TryParse(
$startedAtText,
[Globalization.CultureInfo]::InvariantCulture,
[Globalization.DateTimeStyles]::AssumeUniversal,
[ref]$startedAt)) {
continue
}
$process = Get-Process -Id $pidValue -ErrorAction Stop
$actualStart = [DateTimeOffset]($process.StartTime.ToUniversalTime())
if ([Math]::Abs(($actualStart - $startedAt.ToUniversalTime()).TotalSeconds) -gt 1) {
continue
}
$liveBridges += [PSCustomObject]@{
ProcessId = $pidValue
PipeName = [string]$document.pipeName
BridgeInstanceId = $bridgeInstanceId
StartedAtUnixSeconds = $startedAt.ToUniversalTime().ToUnixTimeSeconds()
}
}
catch {
continue
}
}
$selected = @($liveBridges | Where-Object {
$_.ProcessId -eq $ErpProcessId
})
if ($selected.Count -ne 1) {
throw 'The specified ERP process is not running a valid AgentBridge.'
}
$boundProcessId = [int]$selected[0].ProcessId
$boundStartedAt = [long]$selected[0].StartedAtUnixSeconds
$boundProcess = Get-Process -Id $boundProcessId -ErrorAction Stop
$boundExecutable = [IO.Path]::GetFullPath(
$boundProcess.MainModule.FileName)
if ($boundExecutable -cne $preflightErpExecutable -or
[Math]::Abs((
$boundProcess.StartTime.ToUniversalTime() -
$preflightErpStartedAtUtc).TotalSeconds) -gt 1) {
throw 'The ERP process changed during commercial startup verification.'
}
$boundSessionId = "lserp-pet-p$boundProcessId-s$boundStartedAt-c$expectedSessionScopeToken-$([Guid]::NewGuid().ToString('N'))"
$env:LSERP_ASTRBOT_BASE_URL = $uri.AbsoluteUri.TrimEnd('/')
$env:LSERP_ASTRBOT_SESSION_ID = $boundSessionId
$env:LSERP_AGENT_BRIDGE_PROCESS_ID = [string]$boundProcessId
$env:LSERP_AGENT_EXPECTED_DATABASE_SCOPE_FINGERPRINT =
$ExpectedDatabaseScopeFingerprint
$env:LSERP_AGENT_EXPECTED_USER_ID = $ExpectedUserId
$env:LSERP_AGENT_EXPECTED_USER_NAME = $ExpectedUserName
$env:LSERP_AGENT_EXPECTED_ACCOUNT_BOOK = $ExpectedAccountBook
$env:LSERP_AGENT_EXPECTED_SUBSYSTEM_ID = $ExpectedSubSystemId
$env:LSERP_AGENT_EXPECTED_IS_ADMINISTRATOR = if ($expectedAdministrator) {
'true'
} else { 'false' }
$env:LSERP_AGENT_EXPECTED_SESSION_SCOPE_TOKEN = $expectedSessionScopeToken
$env:LSERP_AGENT_BRIDGE_DISCOVERY = $discoveryRoot
$env:LSERP_ASTRBOT_SPRITE_PATH = $spriteFull
$env:LSERP_PET_SPRITE_PATH = $spriteFull
$env:LSERP_ASTRBOT_CREDENTIAL_TARGET = $CredentialTarget
Start-Process -FilePath $hostPath -WorkingDirectory ([IO.Path]::GetDirectoryName($hostPath))