351 lines
15 KiB
PowerShell
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))
|