diff options
| -rw-r--r-- | CHANGELOG.md | 24 | ||||
| -rw-r--r-- | Directory.Build.props | 2 | ||||
| -rw-r--r-- | README.md | 34 | ||||
| -rw-r--r-- | doc/Design.md | 10 | ||||
| -rw-r--r-- | doc/Specifications.md | 6 | ||||
| -rw-r--r-- | plans/ImplementationPlan.md | 7 | ||||
| -rw-r--r-- | plans/RequirementTraceability.md | 5 | ||||
| -rw-r--r-- | scripts/Operational-E2E.ps1 | 2 | ||||
| -rw-r--r-- | scripts/Publish-Release.ps1 | 208 | ||||
| -rw-r--r-- | scripts/Restart-PublishedServer.ps1 | 2 | ||||
| -rw-r--r-- | scripts/Run-Final-ElevatedVerification.ps1 | 5 | ||||
| -rw-r--r-- | scripts/Verify-AdminIpcAttackMatrix.ps1 | 2 | ||||
| -rw-r--r-- | scripts/Verify-PublishedHttp.ps1 | 2 | ||||
| -rw-r--r-- | src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs | 10 | ||||
| -rw-r--r-- | src/Agent.App/Agent.App.csproj | 2 | ||||
| -rw-r--r-- | src/Agent.Core/Prompts/FixedSystemPrompt.md | 2 | ||||
| -rw-r--r-- | src/Agent.Windows/Diagnostics/WindowsPlatformCheck.cs | 10 | ||||
| -rw-r--r-- | src/Agent.Windows/Security/AppContainerProfile.cs | 2 | ||||
| -rw-r--r-- | tests/Agent.Windows.Tests/AdministrativeBoundaryTests.cs | 11 |
19 files changed, 243 insertions, 103 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9685333 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to WinAgent Sandbox MCP are recorded in this file. + +## Unreleased + +## 0.1.0 — 2026-08-14 + +### Added + +- MCP `agent_run` and `agent_status` tools over stdio and Streamable HTTP. +- Ollama-backed planning, execution, replanning, context management, and localized Japanese/English results. +- YAML-defined workspaces, mounts, approved entry executables, network access, and resource limits. +- Applied Configuration snapshots with resolved paths and executable identities. +- Release Bundle generation for Windows 11 x64 as a self-contained .NET application. + +### Security + +- LPAC identities, NTFS ACLs, Job Objects, and WFP enforcement for the sandbox process tree. +- Entry executable approval while allowing descendants to inherit the same effective sandbox policy. +- A normally privileged `asInvoker` MCP server with task-scoped UAC elevation limited to WFP and loopback operations. +- Versioned, authenticated named-pipe IPC to the fixed `requireAdministrator` helper. +- Transactional ACL and network cleanup with startup recovery and audit correlation. + diff --git a/Directory.Build.props b/Directory.Build.props index f48abd2..c5488e4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,6 +8,8 @@ <MSBuildProjectExtensionsPath>$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath> <DefaultItemExcludes>$(DefaultItemExcludes);**\bin\**;**\obj\**</DefaultItemExcludes> <TargetFramework>net10.0</TargetFramework> + <VersionPrefix>0.1.0</VersionPrefix> + <Product>WinAgent Sandbox MCP</Product> <LangVersion>14.0</LangVersion> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> @@ -1,6 +1,6 @@ -# AI Agent Sandbox MCP Server +# WinAgent Sandbox MCP -Windows上でOllamaを利用し、AIエージェントのファイル操作とプロセス実行を、利用者がYAMLで指定した範囲へ閉じ込めるMCP Serverです。MCP Clientには`agent_run`と`agent_status`だけを公開します。 +Windows 11 x64上でOllamaを利用し、AIエージェントのファイル操作とプロセス実行を利用者がYAMLで指定した範囲へ閉じ込めるMCP Serverです。LPAC、NTFS ACL、Job Object、WFPでSandbox Process Treeを強制し、通常権限のmain processからTask単位で必要なnetwork操作だけをUAC昇格します。MCP Clientには`agent_run`と`agent_status`だけを公開します。 ## 機能 @@ -69,7 +69,7 @@ sandbox: profile: normal ``` -実環境用path、Prompt、Skillは`agent.local.yaml`から指定できます。publish/upgradeは`agent.local.yaml`、`local/`、`logs/`、`state/`、適用済み設定Storeを上書きしません。 +実環境用path、Prompt、Skillは`agent.local.yaml`から指定できます。Release artifact生成用stagingにはこれらの利用者状態を含めません。利用者deploymentをupgradeするときはExecutableと既定資産だけを置換し、`agent.local.yaml`、`local/`、`logs/`、`state/`、適用済み設定Storeを上書きしないでください。 ### 権限マトリックス @@ -164,9 +164,9 @@ stdioでもMCP Clientは通常権限で起動します。ファイル操作だã� ```json { "mcpServers": { - "agent-sandbox": { - "command": "C:\\tools\\agent-sandbox-mcp\\agent-mcp.exe", - "args": ["C:\\tools\\agent-sandbox-mcp\\agent.yaml"] + "winagent-sandbox-mcp": { + "command": "C:\\tools\\winagent-sandbox-mcp\\agent-mcp.exe", + "args": ["C:\\tools\\winagent-sandbox-mcp\\agent.yaml"] } } } @@ -221,6 +221,28 @@ dotnet test AgentSandboxMcp.slnx -c Release --no-build --no-restore .\scripts\Publish-Release.ps1 ``` +## Releaseとupgrade + +`Publish-Release.ps1`は`Directory.Build.props`のApplication Versionを読み取り、次を生成します。展開済みBundle directoryは利用者deploymentではなく、実行のたびに完全再生成されるRelease staging成果物です。 + +```text +build/Release/publish/ + winagent-sandbox-mcp/ + winagent-sandbox-mcp-v<version>-win-x64.zip + winagent-sandbox-mcp-v<version>-win-x64.zip.sha256 +``` + +ZIP内のroot directory名とExecutable名にはversionを含めません。`agent.local.yaml`、`local/`、`logs/`、`state/`、Applied Configuration StoreはBundleとZIPに含まれません。 + +利用者deploymentのupgradeでは、ZIP内のExecutableと既定資産だけを置換し、利用者固有のlocal設定、Prompt、Skill、logs、state、Applied Configuration Storeを保持してください。旧version付きdirectoryから新しいdeployment pathへ移す場合は、local資産を利用者が移動し、新しいconfig pathに対して次を実行します。 + +```powershell +.\agent-mcp.exe show .\agent.yaml +.\agent-mcp.exe apply .\agent.yaml +``` + +本Repositoryはinstallerやdeployment upgrade scriptを提供しません。 + 詳細は[仕様書](doc/Specifications.md)と[設計書](doc/Design.md)を参照してください。 ## ライセンス diff --git a/doc/Design.md b/doc/Design.md index 5a8d0f0..032723e 100644 --- a/doc/Design.md +++ b/doc/Design.md @@ -1,4 +1,4 @@ -# AI Agent Sandbox MCP Server — 設計書 +# WinAgent Sandbox MCP — 設計書 **文書名:** `Design.md` **版:** 0.1 Draft @@ -831,8 +831,12 @@ errorは内部codeとlocalized messageを分離し、ログ/auditにはcode、MC | 10 | Release/Operational E2E | self-contained Bundle単独起動、stdio/HTTP実利用 | ### 21.1 Release設計 -- mainと昇格hostを同じversion/RIDで`dotnet publish -c Release -r win-x64 --self-contained true`し、mainをBundle root、昇格hostを固定`admin/` subdirectoryへ配置する。 -- Bundleはversion付きZIP、展開済みdirectory、file manifest、SHA-256 checksumから成る。 +- Application Versionの正本はRepository rootの`Directory.Build.props`にある`VersionPrefix`とする。Configuration schema versionおよびMCP protocol versionとは独立して管理する。.NET SDK標準のAssemblyVersion、FileVersion、InformationalVersion生成を使用し、Source Revision suffixとWindowsの4要素version表現を許容する。 +- mainと昇格hostを同じApplication Version/RIDで`dotnet publish -c Release -r win-x64 --self-contained true`し、mainを固定名`winagent-sandbox-mcp/`のBundle root、昇格hostを固定`admin/` subdirectoryへ配置する。helper compatibility preflightはmain/helper versionのmajor/minor/buildをparseして比較する。 +- `build/Release/publish/winagent-sandbox-mcp/`はRelease artifact生成用staging成果物であり、利用者deploymentではない。publishごとに一時directoryで完全生成・検査した後、固定Bundle directoryをclean置換する。旧version付きdirectoryは自動移動または削除しない。 +- Bundleはversionを含まない展開済みdirectory、file manifest、version付きZIP、SHA-256 checksumから成る。ZIPの内部rootとExecutable名はversionを持たず、ZIP/checksum filenameだけにApplication Versionを含める。 +- Release BundleとZIPへ`agent.local.yaml`、`local/`、`logs/`、`state/`、Applied Configuration Storeまたは過去のtest/development資産を含めない。生成時とZIPのfresh展開後に禁止項目を検査する。 +- 利用者deploymentのupgradeではExecutableと既定資産だけを置換し、利用者のlocal設定、Prompt、Skill、logs、state、Applied Configuration Storeを上書きしない。installer/deployment upgrade scriptは提供しない。deployment pathを変更した場合は新config pathで`show`後に`apply`する。 - main manifestは`asInvoker`、`admin/agent-mcp-admin.exe` manifestは`requireAdministrator`かつ`uiAccess=false`とする。mainは`AppContext.BaseDirectory/admin/agent-mcp-admin.exe`だけを参照し、PATH検索やConfigurationによる差替えを許さない。 - 起動時の相対pathは実行ファイルdirectoryを基準に解決し、ソースツリーを参照しない。 - 現行Operational E2Eは配布既定の`deepseek-v4-flash:cloud`を使用する。rate limit、接続失敗、model unavailableをapplication PASSとして扱わない。 diff --git a/doc/Specifications.md b/doc/Specifications.md index dcc2425..d127e1f 100644 --- a/doc/Specifications.md +++ b/doc/Specifications.md @@ -1,4 +1,4 @@ -# AI Agent Sandbox MCP Server — 仕様書 +# WinAgent Sandbox MCP — 仕様書 **文書名:** `Specifications.md` **版:** 0.2 Audit Revision @@ -32,6 +32,8 @@ IPA「厳密な仕様記述入門」の考え方に従い、課題・仕様・è¨ 本書は、完全性、一貫性、検証容易性、修正容易性、追跡性を重視する。妥当性確認は「この仕様がAIエージェントの安全な作業実行という目的に適合するか」、正当性検証は「設計・実装が本仕様を満たすか」と定義する。 +Application Version、Configuration schema version、MCP protocol versionは独立して管理する。current Application Versionはbuild metadata、公開履歴はGit tag、CHANGELOG、Release metadataを正本とし、本仕様書へ固定値として持たせない。 + --- ## 2. 目的・課題・範囲 @@ -480,7 +482,7 @@ Resource Profileは`constrained = 1024 MiB/4 Process/300秒`、`normal = 4096 Mi - Hot Reloadは明示的に非対応。 - config pathはCLIの任意の位置引数で選択可能とし、optionによる設定上書きを持たない。 - base設定と同じdirectoryに `agent.local.yaml` が存在する場合は任意overrideとして読み込む。mapは再帰mergeし、scalarと配列はlocal側で置換する。merge後の設定全体をvalidationし、相対pathはbase設定directoryを基準に解決する。 -- Release Bundleは既定設定だけを配布し、利用者のlocal設定、User System Prompt、Skillをpublishで上書きしてはならない。 +- Release artifact生成用stagingは毎回既定配布物だけから完全再生成し、利用者の`agent.local.yaml`、local Prompt/Skill、logs、state、Applied Configuration StoreをRelease BundleまたはZIPへ含めてはならない。利用者deploymentのupgradeではExecutableと既定資産だけを置換し、これらの利用者状態を上書きしてはならない。 - `sandbox.execution.entries`はAlias、Executable Logical Path、optional Runtime Rootを一つのrecordとして持つ。Shell分類はResolved file nameから内部で決定する。 - 起動時にschema、Applied Configuration、locale、Effective Sandbox Policyのvalidationを行う。 - Workspace/Mount ResolutionとExecutable identity/Approvalを分離保存せず、config別Applied Configuration Storeへ一つのsnapshotとして保存する。StoreはWindows user単位のuser-only領域に置き、Release Bundleへ含めない。 diff --git a/plans/ImplementationPlan.md b/plans/ImplementationPlan.md index fc101c7..6a634c7 100644 --- a/plans/ImplementationPlan.md +++ b/plans/ImplementationPlan.md @@ -1,4 +1,4 @@ -# AI Agent Sandbox MCP Server — 実装・監査作業計画 +# WinAgent Sandbox MCP — 実装・監査作業計画 > 2026-08-13オンデマンド昇格追補: Server全体の常時管理者起動を廃止する。`agent-mcp.exe`は`asInvoker`で起動し、Task内の初回`process_run`直前にWFPとloopback exemptionだけをTask単位の`runas` Processへ移す。ACL、LPAC、Job、Executable Approvalは通常側に維持する。WFP dynamic objectのfilter IDは新規Recovery Journalへ保存せず、persistentなloopback状態だけを既存Journalで復元する。旧管理者起動を前提とするPhase完了記録は当時のcommitに対する履歴である。 @@ -564,8 +564,9 @@ Codexの自己申告ではなく、仕様書のAcceptance IDと実成果を1対1 AT-01〜AT-20をすべて再実行する。 ## 16.2.1 Release Bundle -- `dotnet publish -c Release -r win-x64 --self-contained true` を実行する。 -- `agent-mcp.exe`、runtime/dependency、`agent.yaml`、en/ja locale、User Prompt、`skills/`、READMEを含むversion付きZIP、manifest、SHA-256を生成する。 +- `Directory.Build.props`の`VersionPrefix`をApplication Versionの正本とし、main/adminを同じversion/RIDで`dotnet publish -c Release -r win-x64 --self-contained true`する。publish時のVersion overrideは行わない。 +- 一時stagingから固定`winagent-sandbox-mcp/`Bundleを毎回完全再生成し、`agent-mcp.exe`、`admin/agent-mcp-admin.exe`、runtime/dependency、`agent.yaml`、en/ja locale、User Prompt、`skills/`、README、CHANGELOG、manifestを含むversion付きZIPとSHA-256を生成する。 +- stagingと利用者deploymentを分離し、Bundle/ZIPへ`agent.local.yaml`、`local/`、`logs/`、`state/`、Applied Configuration Storeを含めない。利用者deploymentのupgrade方針はREADMEに記載し、installer/upgrade scriptは追加しない。 - ソースツリー外の空directoryへ展開し、Bundle内の資源だけで`agent-mcp show`まで実行する。stdio、HTTPは、利用者がYAMLの権限matrixを確認して`agent-mcp apply`したOperational配置で実行する。 ## 16.2.2 Operational E2E Test diff --git a/plans/RequirementTraceability.md b/plans/RequirementTraceability.md index f0f47f2..79e448a 100644 --- a/plans/RequirementTraceability.md +++ b/plans/RequirementTraceability.md @@ -65,6 +65,11 @@ Internal Tool testは件数を仕様としてassertしない。`Design.md`のToo | Verification | Date / privilege | Result | |---|---|---| +| WinAgent Sandbox MCP v0.1.0 Release build / unit / integration | 2026-08-14 / non-elevated | Release build warning 0/error 0、Core 105 PASS、Planner 10 PASS、administrative boundary 8 PASS、fail/skip 0 | +| WinAgent Sandbox MCP v0.1.0 Windows Security/Recovery | 2026-08-14 / Windows Security childのみadministrator | 84 PASS、fail/skip 0。LPAC、ACL、Job、WFP、Recovery、README operational、PowerShell/cmd/Python descendant matrixを直列実行 | +| WinAgent Sandbox MCP v0.1.0 Operational E2E | 2026-08-14 / main standard-user、helper Task時UAC / `deepseek-v4-flash:cloud` | stdio、HTTP IPv4、HTTP IPv6 PASS。Python 3.14.3、複雑Python実装、subprocess test、ACL/WFP/Journal/Process残留0 | +| WinAgent Sandbox MCP v0.1.0 actual-Process IPC matrix | 2026-08-14 / standard-user+Task単位helper UAC | local peer PID、remote-form pipe、別Task ID、別protocol version、二重apply、過大messageを全件検証してPASS | +| WinAgent Sandbox MCP v0.1.0 Release Bundle | 2026-08-14 / fresh extraction | main/admin Application Version `0.1.0`、Assembly/File `0.1.0.0`、Informational `0.1.0+revision`、固定unversioned root、manifest/checksum一致、利用者状態・local/test資産なし、`show` PASS、旧version付きartifact非変更 | | on-demand elevation Release build | 2026-08-13 / non-elevated | warning 0、error 0 | | on-demand elevation Core / Planner | 2026-08-13 / non-elevated | Core 105 PASS、Planner 10 PASS、fail/skip 0 | | administrative boundary tests | 2026-08-13 / non-elevated | fixed IPC schema、過大/未知field拒否、manifest/native call ownershipの4件PASS、fail/skip 0 | diff --git a/scripts/Operational-E2E.ps1 b/scripts/Operational-E2E.ps1 index cea1c75..2a32a8e 100644 --- a/scripts/Operational-E2E.ps1 +++ b/scripts/Operational-E2E.ps1 @@ -3,7 +3,7 @@ param( [string]$Transport = 'stdio', [ValidateSet('127.0.0.1', '::1')] [string]$HttpHost = '127.0.0.1', - [string]$ReleaseZip = 'build\Release\publish\agent-sandbox-mcp-1.0.0-win-x64.zip', + [string]$ReleaseZip = 'build\Release\publish\winagent-sandbox-mcp-v0.1.0-win-x64.zip', [string]$PythonPath ) diff --git a/scripts/Publish-Release.ps1 b/scripts/Publish-Release.ps1 index 3ab7bda..b34986a 100644 --- a/scripts/Publish-Release.ps1 +++ b/scripts/Publish-Release.ps1 @@ -1,5 +1,4 @@ param( - [string]$Version = '1.0.0', [string]$OutputRoot = 'build\Release\publish', [switch]$SkipRuntimeCheck ) @@ -11,96 +10,159 @@ $buildRootPath = [IO.Path]::GetFullPath((Join-Path $repository 'build')) if (-not $outputRootPath.StartsWith($buildRootPath + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw 'Release output must remain inside the repository build directory.' } -$bundleName = "agent-sandbox-mcp-$Version-win-x64" + +$appProject = Join-Path $repository 'src\Agent.App\Agent.App.csproj' +$adminProject = Join-Path $repository 'src\Agent.Admin\Agent.Admin.Host\Agent.Admin.Host.csproj' +function Get-ApplicationVersion([string]$project) { + $value = (& dotnet msbuild $project -nologo -getProperty:Version | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($value)) { + throw "Could not resolve the Application Version for $project." + } + return $value +} + +$version = Get-ApplicationVersion $appProject +$adminVersion = Get-ApplicationVersion $adminProject +if ($version -ne $adminVersion) { throw "Main/admin Application Version mismatch: $version / $adminVersion" } +if ($version -notmatch '^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$') { + throw "Application Version is not SemVer: $version" +} +$releaseVersion = [Version]($version -replace '[-+].*$', '') + +$bundleName = 'winagent-sandbox-mcp' +$assetBase = "$bundleName-v$version-win-x64" $bundle = Join-Path $outputRootPath $bundleName -$zip = Join-Path $outputRootPath "$bundleName.zip" +$zip = Join-Path $outputRootPath "$assetBase.zip" $checksum = "$zip.sha256" -$stagingRoot = Join-Path $outputRootPath (".staging-" + [Guid]::NewGuid().ToString('N')) +$stagingRoot = Join-Path $outputRootPath ('.staging-' + [Guid]::NewGuid().ToString('N')) $stagingBundle = Join-Path $stagingRoot $bundleName +$forbiddenNames = @('agent.local.yaml', 'local', 'logs', 'state', 'applied-configurations', 'TestResults', 'artifacts', '.staging') + +function Assert-ReleaseIdentity([string]$path, [string]$assemblyPath, [string]$label) { + $info = [Diagnostics.FileVersionInfo]::GetVersionInfo($path) + $fileVersion = [Version]$info.FileVersion + if ($fileVersion.Major -ne $releaseVersion.Major -or + $fileVersion.Minor -ne $releaseVersion.Minor -or + $fileVersion.Build -ne $releaseVersion.Build) { + throw "$label FileVersion '$($info.FileVersion)' does not match Application Version '$version'." + } + $assemblyVersion = [Reflection.AssemblyName]::GetAssemblyName($assemblyPath).Version + if ($assemblyVersion.Major -ne $releaseVersion.Major -or + $assemblyVersion.Minor -ne $releaseVersion.Minor -or + $assemblyVersion.Build -ne $releaseVersion.Build) { + throw "$label AssemblyVersion '$assemblyVersion' does not match Application Version '$version'." + } + if ($info.ProductVersion -notmatch ('^' + [Regex]::Escape($version) + '(?:\+[0-9A-Za-z.-]+)?$')) { + throw "$label InformationalVersion '$($info.ProductVersion)' is not compatible with '$version'." + } + [PSCustomObject]@{ + Label = $label + ApplicationVersion = $version + AssemblyVersion = $assemblyVersion.ToString() + FileVersion = $info.FileVersion + InformationalVersion = $info.ProductVersion + } +} + +function Assert-NoUserState([string]$root) { + $forbidden = Get-ChildItem -LiteralPath $root -Force -Recurse | + Where-Object Name -in $forbiddenNames | + Select-Object -First 1 + if ($forbidden) { + $relative = [IO.Path]::GetRelativePath($root, $forbidden.FullName) + throw "Release artifact must not contain user state, local assets, or test artifacts: $relative" + } +} New-Item -ItemType Directory -Force -Path $outputRootPath | Out-Null if (Test-Path -LiteralPath $zip) { Remove-Item -LiteralPath $zip -Force } if (Test-Path -LiteralPath $checksum) { Remove-Item -LiteralPath $checksum -Force } New-Item -ItemType Directory -Path $stagingBundle | Out-Null -$appProject = Join-Path $repository 'src\Agent.App\Agent.App.csproj' -$adminProject = Join-Path $repository 'src\Agent.Admin\Agent.Admin.Host\Agent.Admin.Host.csproj' -dotnet restore $appProject -r win-x64 --locked-mode -if ($LASTEXITCODE -ne 0) { throw "win-x64 locked restore failed with exit code $LASTEXITCODE" } -dotnet restore $adminProject -r win-x64 --locked-mode -if ($LASTEXITCODE -ne 0) { throw "win-x64 admin locked restore failed with exit code $LASTEXITCODE" } -dotnet publish $appProject -c Release -r win-x64 --self-contained true --no-restore --output $stagingBundle -p:Version=$Version -if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed with exit code $LASTEXITCODE" } -$adminBundle = Join-Path $stagingBundle 'admin' -dotnet publish $adminProject -c Release -r win-x64 --self-contained true --no-restore --output $adminBundle -p:Version=$Version -if ($LASTEXITCODE -ne 0) { throw "admin dotnet publish failed with exit code $LASTEXITCODE" } +try { + dotnet restore $appProject -r win-x64 --locked-mode + if ($LASTEXITCODE -ne 0) { throw "win-x64 locked restore failed with exit code $LASTEXITCODE" } + dotnet restore $adminProject -r win-x64 --locked-mode + if ($LASTEXITCODE -ne 0) { throw "win-x64 admin locked restore failed with exit code $LASTEXITCODE" } + dotnet publish $appProject -c Release -r win-x64 --self-contained true --no-restore --output $stagingBundle + if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed with exit code $LASTEXITCODE" } + $adminBundle = Join-Path $stagingBundle 'admin' + dotnet publish $adminProject -c Release -r win-x64 --self-contained true --no-restore --output $adminBundle + if ($LASTEXITCODE -ne 0) { throw "admin dotnet publish failed with exit code $LASTEXITCODE" } -$required = @( - 'agent-mcp.exe', 'admin\agent-mcp-admin.exe', 'agent.yaml', 'config\locales\en.json', 'config\locales\ja.json', - 'prompts\user-system.md', 'skills', 'README.md' -) -foreach ($relative in $required) { - if (-not (Test-Path -LiteralPath (Join-Path $stagingBundle $relative))) { throw "Release Bundle is missing $relative" } -} -foreach ($relative in @('agent.local.yaml', 'config\agent.local.example.yaml')) { - if (Test-Path -LiteralPath (Join-Path $stagingBundle $relative)) { throw "Release Bundle must not contain local override input: $relative" } -} + $required = @( + 'agent-mcp.exe', 'admin\agent-mcp-admin.exe', 'agent.yaml', 'config\locales\en.json', 'config\locales\ja.json', + 'prompts\user-system.md', 'skills', 'README.md', 'CHANGELOG.md' + ) + foreach ($relative in $required) { + if (-not (Test-Path -LiteralPath (Join-Path $stagingBundle $relative))) { throw "Release Bundle is missing $relative" } + } + Assert-NoUserState $stagingBundle -$manifestPath = Join-Path $stagingBundle 'publish-manifest.sha256' -Get-ChildItem -LiteralPath $stagingBundle -File -Recurse | - Where-Object FullName -ne $manifestPath | - Sort-Object FullName | - ForEach-Object { - $relative = [IO.Path]::GetRelativePath($stagingBundle, $_.FullName).Replace('\', '/') - $hash = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() - "$hash $relative" - } | Set-Content -LiteralPath $manifestPath -Encoding utf8NoBOM + $mainVersionInfo = Assert-ReleaseIdentity (Join-Path $stagingBundle 'agent-mcp.exe') (Join-Path $stagingBundle 'agent-mcp.dll') 'main' + $adminVersionInfo = Assert-ReleaseIdentity (Join-Path $stagingBundle 'admin\agent-mcp-admin.exe') (Join-Path $stagingBundle 'admin\agent-mcp-admin.dll') 'admin' -Compress-Archive -LiteralPath $stagingBundle -DestinationPath $zip -CompressionLevel Optimal -$zipHash = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLowerInvariant() -"$zipHash $([IO.Path]::GetFileName($zip))" | Set-Content -LiteralPath $checksum -Encoding ascii + $manifestPath = Join-Path $stagingBundle 'publish-manifest.sha256' + Get-ChildItem -LiteralPath $stagingBundle -File -Recurse | + Where-Object FullName -ne $manifestPath | + Sort-Object FullName | + ForEach-Object { + $relative = [IO.Path]::GetRelativePath($stagingBundle, $_.FullName).Replace('\', '/') + $hash = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + "$hash $relative" + } | Set-Content -LiteralPath $manifestPath -Encoding utf8NoBOM -$freshRoot = Join-Path ([IO.Path]::GetTempPath()) ("agent-mcp-release-check-" + [Guid]::NewGuid().ToString('N')) -New-Item -ItemType Directory -Path $freshRoot | Out-Null -try { - Expand-Archive -LiteralPath $zip -DestinationPath $freshRoot - $freshBundle = Join-Path $freshRoot $bundleName - $sourceReferences = Get-ChildItem -LiteralPath $freshBundle -File -Recurse | - Where-Object Extension -in @('.yaml', '.json', '.md', '.config') | - Select-String -SimpleMatch -Pattern $repository - if ($sourceReferences) { throw 'Release Bundle contains a source-tree reference.' } - if (-not $SkipRuntimeCheck) { - Push-Location $freshBundle - try { - & .\agent-mcp.exe show .\agent.yaml - if ($LASTEXITCODE -ne 0) { throw "Bundle configuration check failed with exit code $LASTEXITCODE." } + Compress-Archive -LiteralPath $stagingBundle -DestinationPath $zip -CompressionLevel Optimal + $zipHash = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLowerInvariant() + "$zipHash $([IO.Path]::GetFileName($zip))" | Set-Content -LiteralPath $checksum -Encoding ascii + + $freshRoot = Join-Path ([IO.Path]::GetTempPath()) ('winagent-sandbox-mcp-release-check-' + [Guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $freshRoot | Out-Null + try { + Expand-Archive -LiteralPath $zip -DestinationPath $freshRoot + $freshItems = @(Get-ChildItem -LiteralPath $freshRoot -Force) + if ($freshItems.Count -ne 1 -or -not $freshItems[0].PSIsContainer -or $freshItems[0].Name -ne $bundleName) { + throw "ZIP must contain exactly one unversioned root directory named '$bundleName'." + } + $freshBundle = Join-Path $freshRoot $bundleName + Assert-NoUserState $freshBundle + $sourceReferences = Get-ChildItem -LiteralPath $freshBundle -File -Recurse | + Where-Object Extension -in @('.yaml', '.json', '.md', '.config') | + Select-String -SimpleMatch -Pattern $repository + if ($sourceReferences) { throw 'Release Bundle contains a source-tree reference.' } + foreach ($line in Get-Content -LiteralPath (Join-Path $freshBundle 'publish-manifest.sha256')) { + if ($line -notmatch '^([0-9a-f]{64}) (.+)$') { throw "Invalid manifest entry: $line" } + $actual = (Get-FileHash -LiteralPath (Join-Path $freshBundle $Matches[2]) -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $Matches[1]) { throw "Manifest hash mismatch: $($Matches[2])" } + } + if (-not $SkipRuntimeCheck) { + Push-Location $freshBundle + try { + & .\agent-mcp.exe show .\agent.yaml + if ($LASTEXITCODE -ne 0) { throw "Bundle configuration check failed with exit code $LASTEXITCODE." } + } + finally { Pop-Location } } - finally { Pop-Location } } -} -finally { - if (Test-Path -LiteralPath $freshRoot) { Remove-Item -LiteralPath $freshRoot -Recurse -Force } -} + finally { + if (Test-Path -LiteralPath $freshRoot) { Remove-Item -LiteralPath $freshRoot -Recurse -Force } + } -# The ZIP is reproducible defaults only. The deployment directory keeps explicitly local, -# publish-excluded inputs while all versioned defaults and binaries are replaced. -try { - if (Test-Path -LiteralPath $bundle) { - $resolvedBundle = (Resolve-Path -LiteralPath $bundle).Path - if (-not $resolvedBundle.StartsWith($outputRootPath + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { - throw 'Existing deployment Bundle escaped the validated output root.' - } - Get-ChildItem -LiteralPath $resolvedBundle -Force | - Where-Object Name -notin @('agent.local.yaml', 'local', 'logs', 'state') | - Remove-Item -Recurse -Force + $bundleFullPath = [IO.Path]::GetFullPath($bundle) + if (-not $bundleFullPath.StartsWith($outputRootPath + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or + [IO.Path]::GetFileName($bundleFullPath) -ne $bundleName) { + throw 'Fixed Release Bundle target escaped the validated output root.' } - else { New-Item -ItemType Directory -Path $bundle | Out-Null } - Copy-Item -Path (Join-Path $stagingBundle '*') -Destination $bundle -Recurse -Force + if (Test-Path -LiteralPath $bundleFullPath) { Remove-Item -LiteralPath $bundleFullPath -Recurse -Force } + Move-Item -LiteralPath $stagingBundle -Destination $bundleFullPath + Assert-NoUserState $bundleFullPath + + $mainVersionInfo + $adminVersionInfo + Write-Output "Bundle: $bundle" + Write-Output "ZIP: $zip" + Write-Output "SHA-256: $zipHash" } finally { if (Test-Path -LiteralPath $stagingRoot) { Remove-Item -LiteralPath $stagingRoot -Recurse -Force } } - -Write-Output "Bundle: $bundle" -Write-Output "ZIP: $zip" -Write-Output "SHA-256: $checksum" diff --git a/scripts/Restart-PublishedServer.ps1 b/scripts/Restart-PublishedServer.ps1 index d4386f4..1909adf 100644 --- a/scripts/Restart-PublishedServer.ps1 +++ b/scripts/Restart-PublishedServer.ps1 @@ -1,5 +1,5 @@ param( - [string]$BundlePath = 'build\Release\publish\agent-sandbox-mcp-1.0.0-win-x64', + [string]$BundlePath = 'build\Release\publish\winagent-sandbox-mcp', [switch]$StopOnly ) diff --git a/scripts/Run-Final-ElevatedVerification.ps1 b/scripts/Run-Final-ElevatedVerification.ps1 index 1162da0..286d459 100644 --- a/scripts/Run-Final-ElevatedVerification.ps1 +++ b/scripts/Run-Final-ElevatedVerification.ps1 @@ -33,8 +33,9 @@ function Invoke-VerificationStep { "START $Name $(Get-Date -Format o)" | Tee-Object -FilePath $path $exitCode = 0 try { + $global:LASTEXITCODE = 0 & $Action *>&1 | Tee-Object -FilePath $path -Append - if ($LASTEXITCODE) { $exitCode = $LASTEXITCODE } + if ($global:LASTEXITCODE) { $exitCode = $global:LASTEXITCODE } } catch { $_ | Out-String | Tee-Object -FilePath $path -Append @@ -44,7 +45,7 @@ function Invoke-VerificationStep { if ($exitCode -ne 0) { $script:failed = $true } } -$bundle = Join-Path $repository 'build\Release\publish\agent-sandbox-mcp-1.0.0-win-x64' +$bundle = Join-Path $repository 'build\Release\publish\winagent-sandbox-mcp' $windowsTests = Join-Path $repository 'build\Release\Agent.Windows.Tests\net10.0-windows10.0.26100.0\Agent.Windows.Tests.exe' $coreTests = Join-Path $repository 'build\Release\Agent.Core.Tests\net10.0-windows10.0.26100.0\Agent.Core.Tests.exe' $plannerTests = Join-Path $repository 'build\Release\Agent.Planner.Tests\net10.0\Agent.Planner.Tests.exe' diff --git a/scripts/Verify-AdminIpcAttackMatrix.ps1 b/scripts/Verify-AdminIpcAttackMatrix.ps1 index c0703f0..0b3957a 100644 --- a/scripts/Verify-AdminIpcAttackMatrix.ps1 +++ b/scripts/Verify-AdminIpcAttackMatrix.ps1 @@ -1,6 +1,6 @@ param( [string]$Configuration = 'Release', - [string]$BundlePath = 'build\Release\publish\agent-sandbox-mcp-1.0.0-win-x64' + [string]$BundlePath = 'build\Release\publish\winagent-sandbox-mcp' ) $ErrorActionPreference = 'Stop' diff --git a/scripts/Verify-PublishedHttp.ps1 b/scripts/Verify-PublishedHttp.ps1 index 414e749..584cbd7 100644 --- a/scripts/Verify-PublishedHttp.ps1 +++ b/scripts/Verify-PublishedHttp.ps1 @@ -1,5 +1,5 @@ param( - [string]$BundlePath = 'build\Release\publish\agent-sandbox-mcp-1.0.0-win-x64', + [string]$BundlePath = 'build\Release\publish\winagent-sandbox-mcp', [string]$TestRoot = 'C:\tmp', [string]$EvidenceRoot = 'artifacts\evidence\published-http-cmd-verification' ) diff --git a/src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs b/src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs index 4aa21da..3961e83 100644 --- a/src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs +++ b/src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs @@ -20,7 +20,7 @@ public sealed unsafe class WfpDynamicSession : IDisposable private readonly List<ulong> _filterIds = []; private WfpDynamicSession(FwpmEngineClose0SafeHandle engine) => _engine = engine; - public static WfpDynamicSession Open(string displayName = "Agent Sandbox MCP dynamic task session") + public static WfpDynamicSession Open(string displayName = "WinAgent Sandbox MCP dynamic task session") { FWPM_SESSION0 session = default; session.flags = PInvoke.FWPM_SESSION_FLAG_DYNAMIC; @@ -48,7 +48,7 @@ public sealed unsafe class WfpDynamicSession : IDisposable try { Guid subLayerKey = Guid.NewGuid(); - fixed (char* subLayerName = "Agent Sandbox MCP task sublayer") + fixed (char* subLayerName = "WinAgent Sandbox MCP task sublayer") { FWPM_SUBLAYER0 subLayer = default; subLayer.subLayerKey = subLayerKey; @@ -60,8 +60,8 @@ public sealed unsafe class WfpDynamicSession : IDisposable fixed (byte* sidPointer = sidBytes) { foreach (NetworkEndpoint endpoint in allowedEndpoints ?? []) _filterIds.Add(AddPackagePermit(endpoint, subLayerKey, (SID*)sidPointer)); - _filterIds.Add(AddPackageBlock(PInvoke.FWPM_LAYER_ALE_AUTH_CONNECT_V4, subLayerKey, (SID*)sidPointer, "Agent Sandbox MCP IPv4 default block")); - _filterIds.Add(AddPackageBlock(PInvoke.FWPM_LAYER_ALE_AUTH_CONNECT_V6, subLayerKey, (SID*)sidPointer, "Agent Sandbox MCP IPv6 default block")); + _filterIds.Add(AddPackageBlock(PInvoke.FWPM_LAYER_ALE_AUTH_CONNECT_V4, subLayerKey, (SID*)sidPointer, "WinAgent Sandbox MCP IPv4 default block")); + _filterIds.Add(AddPackageBlock(PInvoke.FWPM_LAYER_ALE_AUTH_CONNECT_V6, subLayerKey, (SID*)sidPointer, "WinAgent Sandbox MCP IPv6 default block")); } ThrowIfError(PInvoke.FwpmTransactionCommit0(_engine), "FwpmTransactionCommit0"); committed = true; @@ -159,7 +159,7 @@ public sealed unsafe class WfpDynamicSession : IDisposable conditions[2].matchType = FWP_MATCH_TYPE.FWP_MATCH_EQUAL; conditions[2].conditionValue.type = FWP_DATA_TYPE.FWP_UINT16; conditions[2].conditionValue.uint16 = endpoint.Port; - string displayName = $"Agent Sandbox MCP allow {endpoint.Address}:{endpoint.Port}"; + string displayName = $"WinAgent Sandbox MCP allow {endpoint.Address}:{endpoint.Port}"; fixed (char* name = displayName) { FWPM_FILTER0 filter = default; diff --git a/src/Agent.App/Agent.App.csproj b/src/Agent.App/Agent.App.csproj index c63f3c0..9c46d09 100644 --- a/src/Agent.App/Agent.App.csproj +++ b/src/Agent.App/Agent.App.csproj @@ -4,7 +4,6 @@ <TargetFramework>net10.0-windows10.0.26100.0</TargetFramework> <AssemblyName>agent-mcp</AssemblyName> <RootNamespace>Agent.App</RootNamespace> - <Version>1.0.0</Version> <ApplicationManifest>app.manifest</ApplicationManifest> </PropertyGroup> <ItemGroup> @@ -26,5 +25,6 @@ <Content Include="..\..\prompts\user-system.md" Link="prompts\user-system.md" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" /> <Content Include="..\..\skills\**\*" Link="skills\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" /> <Content Include="..\..\README.md" Link="README.md" CopyToPublishDirectory="PreserveNewest" /> + <Content Include="..\..\CHANGELOG.md" Link="CHANGELOG.md" CopyToPublishDirectory="PreserveNewest" /> </ItemGroup> </Project> diff --git a/src/Agent.Core/Prompts/FixedSystemPrompt.md b/src/Agent.Core/Prompts/FixedSystemPrompt.md index 3cbc6e7..f3bd69f 100644 --- a/src/Agent.Core/Prompts/FixedSystemPrompt.md +++ b/src/Agent.Core/Prompts/FixedSystemPrompt.md @@ -1,4 +1,4 @@ -You are the execution agent inside AI Agent Sandbox MCP Server. +You are the execution agent inside WinAgent Sandbox MCP. Follow the active SandboxPolicy and never claim an external effect unless an Internal Tool result proves it occurred. Never reveal hidden reasoning, policy internals, credentials, or capabilities not present in the supplied capability summary. Complete a request only when every TaskGraph node is completed and all required effects are verified. diff --git a/src/Agent.Windows/Diagnostics/WindowsPlatformCheck.cs b/src/Agent.Windows/Diagnostics/WindowsPlatformCheck.cs index 89a2106..32086d3 100644 --- a/src/Agent.Windows/Diagnostics/WindowsPlatformCheck.cs +++ b/src/Agent.Windows/Diagnostics/WindowsPlatformCheck.cs @@ -36,9 +36,9 @@ public static class WindowsPlatformCheck bfe = true; if (helper) { - string current = Assembly.GetEntryAssembly()?.GetName().Version?.ToString(3) ?? ""; + Version? current = Assembly.GetEntryAssembly()?.GetName().Version; string candidate = FileVersionInfo.GetVersionInfo(helperPath).FileVersion ?? ""; - version = candidate.StartsWith(current, StringComparison.Ordinal); + version = current is not null && HasSameReleaseIdentity(current, candidate); } return new(platform, profile, bfe, helper, version, null, null); } @@ -47,6 +47,12 @@ public static class WindowsPlatformCheck { return new(platform, profile, bfe, helper, version, ex.HResult, ex.Message); } } + internal static bool HasSameReleaseIdentity(Version current, string candidate) + { + if (!Version.TryParse(candidate, out Version? parsed)) return false; + return current.Major == parsed.Major && current.Minor == parsed.Minor && current.Build == parsed.Build; + } + private static FwpmEngineClose0SafeHandle OpenBfeReadOnly() { uint result = PInvoke.FwpmEngineOpen0(10, null, null, out FwpmEngineClose0SafeHandle engine); diff --git a/src/Agent.Windows/Security/AppContainerProfile.cs b/src/Agent.Windows/Security/AppContainerProfile.cs index 97258ec..83b1675 100644 --- a/src/Agent.Windows/Security/AppContainerProfile.cs +++ b/src/Agent.Windows/Security/AppContainerProfile.cs @@ -14,7 +14,7 @@ public sealed partial class AppContainerProfile public static AppContainerProfile EnsureFixed() { - int result = Native.CreateAppContainerProfile(FixedName, "Agent Sandbox MCP v1", "Fixed sandbox profile for Agent Sandbox MCP", 0, 0, out nint sidPointer); + int result = Native.CreateAppContainerProfile(FixedName, "WinAgent Sandbox MCP", "Fixed sandbox profile for WinAgent Sandbox MCP", 0, 0, out nint sidPointer); if (result == AlreadyExists) result = Native.DeriveAppContainerSidFromAppContainerName(FixedName, out sidPointer); if (result < 0) Marshal.ThrowExceptionForHR(result); try diff --git a/tests/Agent.Windows.Tests/AdministrativeBoundaryTests.cs b/tests/Agent.Windows.Tests/AdministrativeBoundaryTests.cs index b218072..60ff426 100644 --- a/tests/Agent.Windows.Tests/AdministrativeBoundaryTests.cs +++ b/tests/Agent.Windows.Tests/AdministrativeBoundaryTests.cs @@ -2,6 +2,7 @@ using System.Buffers.Binary; using System.Text; using System.Text.Json; using Agent.Admin.Client; +using Agent.Windows.Diagnostics; namespace Agent.Windows.Tests; @@ -74,6 +75,16 @@ public sealed class AdministrativeBoundaryTests Assert.Contains("PipeRejectRemoteClients", client, StringComparison.Ordinal); } + [Theory] + [InlineData("0.1.0.0", true)] + [InlineData("0.1.0", true)] + [InlineData("0.1.1.0", false)] + [InlineData("invalid", false)] + public void Helper_compatibility_compares_parsed_major_minor_build(string helperFileVersion, bool expected) + { + Assert.Equal(expected, WindowsPlatformCheck.HasSameReleaseIdentity(new Version(0, 1, 0, 0), helperFileVersion)); + } + private static string RepositoryRoot() { DirectoryInfo? current = new(AppContext.BaseDirectory); |
