[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))