feat: ship agent-first mission control v0.2.57
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
#requires -Version 7.0
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet("Pin", "Baseline", "Candidate")]
|
||||
[string]$Mode = "Pin",
|
||||
[string]$CandidateVersion = "2.0.0",
|
||||
[switch]$KeepTemporaryCopy
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$stableVersion = "1.4.1"
|
||||
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
||||
$projectPath = Join-Path $repoRoot "backend\Nexus.Api.csproj"
|
||||
$testProjectPath = Join-Path $repoRoot "backend-tests\Nexus.Api.Tests.csproj"
|
||||
$projectHashBefore = (Get-FileHash -LiteralPath $projectPath -Algorithm SHA256).Hash
|
||||
$temporaryRoot = $null
|
||||
$dotnetExe = $null
|
||||
|
||||
function Get-McpPackageVersion {
|
||||
param([Parameter(Mandatory)][string]$Path)
|
||||
|
||||
[xml]$project = Get-Content -LiteralPath $Path -Raw
|
||||
$reference = @(
|
||||
@($project.Project.ItemGroup.PackageReference) |
|
||||
Where-Object { $_.Include -eq "ModelContextProtocol.AspNetCore" }
|
||||
)
|
||||
if ($reference.Count -ne 1) {
|
||||
throw "Expected exactly one ModelContextProtocol.AspNetCore reference in $Path."
|
||||
}
|
||||
return $reference[0].GetAttribute("Version")
|
||||
}
|
||||
|
||||
function Assert-StaticGate {
|
||||
$version = Get-McpPackageVersion -Path $projectPath
|
||||
if ($version -ne $stableVersion) {
|
||||
throw "MCP production pin must remain $stableVersion; found $version."
|
||||
}
|
||||
|
||||
$testFiles = @(
|
||||
"backend-tests\McpServerConfigurationTests.cs",
|
||||
"backend-tests\McpToolsTests.cs",
|
||||
"backend-tests\AgentProposalServiceTests.cs"
|
||||
)
|
||||
foreach ($relativePath in $testFiles) {
|
||||
$path = Join-Path $repoRoot $relativePath
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
||||
throw "Required MCP compatibility coverage is missing: $relativePath"
|
||||
}
|
||||
}
|
||||
|
||||
$toolSource = Get-Content -LiteralPath (Join-Path $repoRoot "backend\Services\NexusMcpTools.cs") -Raw
|
||||
foreach ($marker in @(
|
||||
"nexus_propose_agent",
|
||||
"nexus_get_agent_proposal",
|
||||
"UseStructuredContent = true",
|
||||
"OutputSchemaType = typeof(AgentProposalToolResult)",
|
||||
"Idempotent = true",
|
||||
"OpenWorld = false"
|
||||
)) {
|
||||
if (-not $toolSource.Contains($marker, [StringComparison]::Ordinal)) {
|
||||
throw "Required MCP contract marker is missing: $marker"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-DotNet10 {
|
||||
$candidates = @()
|
||||
$pathDotnet = Get-Command dotnet -ErrorAction SilentlyContinue
|
||||
if ($pathDotnet) {
|
||||
$candidates += $pathDotnet.Source
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) {
|
||||
$candidates += (Join-Path $env:USERPROFILE ".dotnet\dotnet.exe")
|
||||
}
|
||||
|
||||
foreach ($candidate in $candidates | Select-Object -Unique) {
|
||||
if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
|
||||
continue
|
||||
}
|
||||
$sdks = & $candidate --list-sdks
|
||||
if ($LASTEXITCODE -eq 0 -and ($sdks | Where-Object { $_ -match "^10\." })) {
|
||||
return (Resolve-Path -LiteralPath $candidate).Path
|
||||
}
|
||||
}
|
||||
|
||||
throw ".NET SDK 10 is required for Baseline and Candidate modes. Add it to PATH or install it under USERPROFILE\\.dotnet."
|
||||
}
|
||||
|
||||
function Invoke-DotNetTest {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Project,
|
||||
[Parameter(Mandatory)][string]$WorkingDirectory
|
||||
)
|
||||
|
||||
Push-Location $WorkingDirectory
|
||||
try {
|
||||
& $script:dotnetExe test $Project `
|
||||
--configuration Release `
|
||||
--verbosity minimal
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet test failed for $Project."
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-SourceTree {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Source,
|
||||
[Parameter(Mandatory)][string]$Destination
|
||||
)
|
||||
|
||||
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
|
||||
$sourceRoot = [System.IO.Path]::GetFullPath($Source)
|
||||
Get-ChildItem -LiteralPath $sourceRoot -Recurse -File |
|
||||
Where-Object {
|
||||
$relative = [System.IO.Path]::GetRelativePath($sourceRoot, $_.FullName)
|
||||
$segments = $relative -split "[\\/]"
|
||||
-not ($segments -contains "bin" -or $segments -contains "obj")
|
||||
} |
|
||||
ForEach-Object {
|
||||
$relative = [System.IO.Path]::GetRelativePath($sourceRoot, $_.FullName)
|
||||
$target = Join-Path $Destination $relative
|
||||
$targetDirectory = Split-Path -Parent $target
|
||||
New-Item -ItemType Directory -Path $targetDirectory -Force | Out-Null
|
||||
Copy-Item -LiteralPath $_.FullName -Destination $target -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Set-CandidatePackageVersion {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Path,
|
||||
[Parameter(Mandatory)][string]$Version
|
||||
)
|
||||
|
||||
[xml]$project = Get-Content -LiteralPath $Path -Raw
|
||||
$reference = @(
|
||||
@($project.Project.ItemGroup.PackageReference) |
|
||||
Where-Object { $_.Include -eq "ModelContextProtocol.AspNetCore" }
|
||||
)
|
||||
if ($reference.Count -ne 1) {
|
||||
throw "Candidate copy does not contain exactly one MCP package reference."
|
||||
}
|
||||
$reference[0].SetAttribute("Version", $Version)
|
||||
$project.Save($Path)
|
||||
}
|
||||
|
||||
try {
|
||||
Assert-StaticGate
|
||||
Write-Host "PASS: MCP production dependency is pinned to $stableVersion and required source/test markers are present."
|
||||
Write-Host "Static Pin mode did not execute tests or a live MCP/OpenClaw negotiation."
|
||||
|
||||
if ($Mode -eq "Pin") {
|
||||
return
|
||||
}
|
||||
|
||||
$dotnetExe = Resolve-DotNet10
|
||||
# MSBuild launches helper applications (for example the OpenAPI document
|
||||
# generator) through `dotnet`, not through the absolute executable used by
|
||||
# this script. Keep those child processes on the same .NET 10 runtime.
|
||||
$dotnetRoot = Split-Path -Parent $dotnetExe
|
||||
$env:DOTNET_ROOT = $dotnetRoot
|
||||
$pathSeparator = [System.IO.Path]::PathSeparator
|
||||
if (-not (($env:PATH -split [Regex]::Escape([string]$pathSeparator)) -contains $dotnetRoot)) {
|
||||
$env:PATH = "$dotnetRoot$pathSeparator$env:PATH"
|
||||
}
|
||||
Invoke-DotNetTest -Project $testProjectPath -WorkingDirectory $repoRoot
|
||||
Write-Host "PASS: MCP $stableVersion baseline command completed successfully."
|
||||
Write-Warning "Environment-gated Docker, Toxiproxy, or live OpenClaw tests may still be skipped; inspect the test summary before treating this as full compatibility evidence."
|
||||
|
||||
if ($Mode -eq "Baseline") {
|
||||
return
|
||||
}
|
||||
|
||||
if ($CandidateVersion -notmatch "^2\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$") {
|
||||
throw "CandidateVersion must be an explicit MCP 2.x package version."
|
||||
}
|
||||
|
||||
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) "nexus-mcp2-gate-$([Guid]::NewGuid().ToString('N'))"
|
||||
New-Item -ItemType Directory -Path $temporaryRoot -Force | Out-Null
|
||||
Copy-SourceTree -Source (Join-Path $repoRoot "backend") -Destination (Join-Path $temporaryRoot "backend")
|
||||
Copy-SourceTree -Source (Join-Path $repoRoot "backend-tests") -Destination (Join-Path $temporaryRoot "backend-tests")
|
||||
|
||||
$candidateProject = Join-Path $temporaryRoot "backend\Nexus.Api.csproj"
|
||||
$candidateTests = Join-Path $temporaryRoot "backend-tests\Nexus.Api.Tests.csproj"
|
||||
Set-CandidatePackageVersion -Path $candidateProject -Version $CandidateVersion
|
||||
|
||||
& $dotnetExe restore $candidateTests --force-evaluate
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "MCP $CandidateVersion restore failed in the isolated copy."
|
||||
}
|
||||
|
||||
$assetsPath = Join-Path $temporaryRoot "backend\obj\project.assets.json"
|
||||
if (-not (Test-Path -LiteralPath $assetsPath -PathType Leaf)) {
|
||||
throw "Candidate restore did not create project.assets.json."
|
||||
}
|
||||
$resolvedMarker = "`"ModelContextProtocol.AspNetCore/$CandidateVersion`""
|
||||
$assets = Get-Content -LiteralPath $assetsPath -Raw
|
||||
if (-not $assets.Contains($resolvedMarker, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Restore did not resolve the exact requested candidate $CandidateVersion."
|
||||
}
|
||||
|
||||
Invoke-DotNetTest -Project $candidateTests -WorkingDirectory $temporaryRoot
|
||||
|
||||
if ($CandidateVersion.Contains("-", [StringComparison]::Ordinal)) {
|
||||
Write-Warning "COMPATIBILITY PROBE PASS: $CandidateVersion compiled and passed all backend tests."
|
||||
Write-Warning "PROMOTION GATE CLOSED: prerelease packages are not approved for production. The repository remains on $stableVersion."
|
||||
} else {
|
||||
Write-Host "SDK COMPATIBILITY PROBE PASS: stable candidate $CandidateVersion compiled and passed the backend suite."
|
||||
Write-Warning "LIVE OPENCLAW GATE CLOSED: this probe does not test OpenClaw tools/list, client identity, Streamable HTTP, or protocol down-level negotiation."
|
||||
Write-Host "The repository was not modified; live isolated OpenClaw compatibility evidence and a separate reviewed dependency change are still required."
|
||||
}
|
||||
} finally {
|
||||
$projectHashAfter = (Get-FileHash -LiteralPath $projectPath -Algorithm SHA256).Hash
|
||||
if ($projectHashAfter -ne $projectHashBefore) {
|
||||
throw "Safety invariant violated: $projectPath changed during the compatibility gate."
|
||||
}
|
||||
|
||||
if ($temporaryRoot -and (Test-Path -LiteralPath $temporaryRoot)) {
|
||||
if ($KeepTemporaryCopy) {
|
||||
Write-Host "Temporary candidate copy retained at $temporaryRoot"
|
||||
} else {
|
||||
$resolvedTemporaryRoot = [System.IO.Path]::GetFullPath($temporaryRoot)
|
||||
$systemTemporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
|
||||
if (
|
||||
-not $resolvedTemporaryRoot.StartsWith($systemTemporaryRoot, [StringComparison]::OrdinalIgnoreCase) -or
|
||||
-not ([System.IO.Path]::GetFileName($resolvedTemporaryRoot)).StartsWith(
|
||||
"nexus-mcp2-gate-",
|
||||
[StringComparison]::Ordinal)
|
||||
) {
|
||||
throw "Refusing to remove an unverified temporary path: $resolvedTemporaryRoot"
|
||||
}
|
||||
Remove-Item -LiteralPath $resolvedTemporaryRoot -Recurse -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user