diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-08-13 18:20:33 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-08-13 18:20:33 +0900 |
| commit | 76fdcb43585a4c4d288c1026d318a0ddbe3e26ab (patch) | |
| tree | f58296832d37b035d3e787e0f270c77b59c89d77 /scripts | |
| parent | 78fe349073e3a2e89f0bf14b134ae0955aa7182a (diff) | |
fix(sandbox): correct cmd descendant handling and protected ACL policy
Replace the earlier cmd-wide descendant incompatibility diagnosis with the verified distinction between drive-qualified paths and PATH/workspace-relative invocation. Document the cmd YAML configuration, executable resolution, --no-runtime approval flow, and safe descendant invocation examples in the user README.
Keep the System32 protected DACL intact for both the OS Runtime Root and approved entry files nested below it. Skip redundant file-level ACL rewrites for approved System32 executables while retaining LPAC confinement, resolved-path checks, SHA-256 approval, WFP policy, and Job Object enforcement.
Expand Windows coverage with capability positive/negative matrices, protected-DACL regression tests, cmd/PowerShell/venv Python script execution, nine Entry-to-Descendant combinations, network none/local probes, and randomized ACL round-trip testing. Add read-only CLI/README verification and published HTTP MCP agent_run verification scripts.
Update Design, Audit Report, and Requirement Traceability with the corrected root cause, current evidence, withdrawn diagnostic conclusions, and the remaining partial item for per-combination descendant self-recorded LPAC SID/Job evidence.
Verified: Release build 0 warnings/errors; Core 103 PASS; Planner 10 PASS; Windows 75 PASS with skip 0; README operational suite 19 PASS; published HTTP MCP agent_run completed 3/3 with cmd, curl, C:\tmp\.venv Python, network-none enforcement, ACL restoration, and CMD_HTTP_MCP_OK.
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/Verify-PublishedHttp.ps1 | 87 | ||||
| -rw-r--r-- | scripts/Verify-Readme.ps1 | 258 |
2 files changed, 345 insertions, 0 deletions
diff --git a/scripts/Verify-PublishedHttp.ps1 b/scripts/Verify-PublishedHttp.ps1 new file mode 100644 index 0000000..a02a28b --- /dev/null +++ b/scripts/Verify-PublishedHttp.ps1 @@ -0,0 +1,87 @@ +param( + [string]$BundlePath = 'build\Release\publish\agent-sandbox-mcp-1.0.0-win-x64', + [string]$TestRoot = 'C:\tmp', + [string]$EvidenceRoot = 'artifacts\evidence\published-http-cmd-verification' +) + +$ErrorActionPreference = 'Stop' +$OutputEncoding = [Text.UTF8Encoding]::new($false) +[Console]::OutputEncoding = $OutputEncoding +$repository = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +$bundle = [IO.Path]::GetFullPath((Join-Path $repository $BundlePath)) +$publishRoot = [IO.Path]::GetFullPath((Join-Path $repository 'build\Release\publish')) +if (-not $bundle.StartsWith($publishRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { + throw 'BundlePath must remain inside build\Release\publish.' +} +$testRootPath = (Resolve-Path -LiteralPath $TestRoot).Path +$evidence = [IO.Path]::GetFullPath((Join-Path $repository $EvidenceRoot)) +if (-not $evidence.StartsWith($repository + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { + throw 'Evidence output must remain inside the repository.' +} +$administrator = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $administrator) { throw 'Published HTTP verification must run as Windows Administrator.' } + +$agent = Join-Path $bundle 'agent-mcp.exe' +$config = Join-Path $bundle 'agent.yaml' +$clientProject = Join-Path $repository 'scripts\E2EClient\E2EClient.csproj' +$runRoot = Join-Path $testRootPath ('agent-mcp-http-cmd-' + [Guid]::NewGuid().ToString('N')) +$runRootForward = $runRoot.Replace('\', '/') +$endpoint = 'http://127.0.0.1:8080/mcp' +$log = Join-Path $evidence 'verification.log' +$server = $null +New-Item -ItemType Directory -Force -Path $evidence | Out-Null +New-Item -ItemType Directory -Path $runRoot | Out-Null +$aclBefore = (Get-Acl -LiteralPath $runRoot).Sddl + +$request = @" +実作業を行ってください。作業ディレクトリは $runRootForward です。 +file_editで probe.py と run-probe.cmd を作成してください。 +probe.pyはsys.prefixがC:\tmp\.venvであることを検証し、VENV_PYTHON_OKと表示してください。 +run-probe.cmdは次を順番に実行し、どれかが失敗したら非0で終了してください。 +1. 非修飾名 cmd.exe を子Processとして実行する。 +2. 非修飾名 curl.exe --version を子Processとして実行する。 +3. curl.exeで $endpoint へ接続を試み、sandbox.network.access=noneにより失敗することを確認する。接続できた場合は失敗とする。 +4. Workspace相対path ..\.venv\Scripts\python.exe でprobe.pyを実行する。 +最後にInternal Tool process_runを executable=cmd、cwd=$runRootForward、args=["/d","/c","call run-probe.cmd"] で実行し、exit code 0とCMD_HTTP_MCP_OKを確認してください。説明だけで完了せず、必ずファイル作成とprocess_runを実行してください。 +"@ + +try { + & $agent check --config $config --lang ja 2>&1 | Tee-Object -FilePath $log + if ($LASTEXITCODE -ne 0) { throw "Published check failed with exit code $LASTEXITCODE." } + + $server = Start-Process -FilePath $agent -ArgumentList @('serve','--config',$config,'--lang','ja','--transport','http') ` + -WorkingDirectory $bundle -WindowStyle Hidden -PassThru + $ready = $false + for ($attempt = 0; $attempt -lt 100; $attempt++) { + if ($server.HasExited) { throw "Published server exited before readiness: $($server.ExitCode)" } + try { + $response = Invoke-WebRequest -Uri $endpoint -Method Get -SkipHttpErrorCheck + if ($response.StatusCode -eq 405) { $ready = $true; break } + } + catch { } + Start-Sleep -Milliseconds 100 + } + if (-not $ready) { throw 'Published HTTP MCP server did not become ready.' } + + dotnet run --project $clientProject -c Release --no-build -- http $endpoint unused $request 2>&1 | + Tee-Object -FilePath $log -Append + if ($LASTEXITCODE -ne 0) { throw "HTTP MCP agent_run failed with exit code $LASTEXITCODE." } + if (-not (Test-Path -LiteralPath (Join-Path $runRoot 'probe.py')) -or + -not (Test-Path -LiteralPath (Join-Path $runRoot 'run-probe.cmd'))) { + throw 'MCP Task did not create the required verification files.' + } +} +finally { + if ($server -and -not $server.HasExited) { $server.Kill($true); $server.WaitForExit() } + if (Test-Path -LiteralPath $runRoot) { + if ((Get-Acl -LiteralPath $runRoot).Sddl -ne $aclBefore) { throw 'MCP Task did not restore the test directory ACL.' } + $resolvedRunRoot = (Resolve-Path -LiteralPath $runRoot).Path + if (-not $resolvedRunRoot.StartsWith($testRootPath.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase)) { + throw 'Refusing to clean a test directory outside TestRoot.' + } + Remove-Item -LiteralPath $resolvedRunRoot -Recurse -Force + } +} + +Write-Output "Published HTTP MCP cmd verification PASS. Evidence: $evidence" diff --git a/scripts/Verify-Readme.ps1 b/scripts/Verify-Readme.ps1 new file mode 100644 index 0000000..f67f640 --- /dev/null +++ b/scripts/Verify-Readme.ps1 @@ -0,0 +1,258 @@ +param( + [Parameter(Mandatory)] + [string]$TestRoot, + [Parameter(Mandatory)] + [string]$VenvPython, + [string]$EvidenceRoot = 'artifacts\evidence\readme-verification', + [switch]$CliOnly +) + +$ErrorActionPreference = 'Stop' +$OutputEncoding = [Text.UTF8Encoding]::new($false) +[Console]::OutputEncoding = $OutputEncoding +$repository = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +$testRootPath = (Resolve-Path -LiteralPath $TestRoot).Path +$venvPythonPath = (Resolve-Path -LiteralPath $VenvPython).Path +$evidence = [IO.Path]::GetFullPath((Join-Path $repository $EvidenceRoot)) +if (-not $evidence.StartsWith($repository + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { + throw 'Evidence output must remain inside the repository.' +} +$administrator = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $administrator -and -not $CliOnly) { throw 'README verification must run as Windows Administrator unless -CliOnly is specified.' } +if (-not (Test-Path -LiteralPath $venvPythonPath -PathType Leaf)) { throw 'VenvPython must be an existing file.' } + +$pwshPath = (Get-Command pwsh.exe -ErrorAction Stop).Source +$pwshRuntime = Split-Path $pwshPath +$cmdPath = $env:ComSpec +$venvRoot = Split-Path (Split-Path $venvPythonPath) +$pyvenv = Get-Content -LiteralPath (Join-Path $venvRoot 'pyvenv.cfg') +$pythonRuntime = (($pyvenv | Where-Object { $_ -match '^home\s*=' }) -split '=', 2)[1].Trim() +if (-not (Test-Path -LiteralPath $pythonRuntime -PathType Container)) { throw "Python Runtime Root does not exist: $pythonRuntime" } +$basePythonPath = Join-Path $pythonRuntime 'python.exe' +if (-not (Test-Path -LiteralPath $basePythonPath -PathType Leaf)) { throw "Base Python does not exist: $basePythonPath" } + +New-Item -ItemType Directory -Force -Path $evidence | Out-Null +$runRoot = Join-Path $testRootPath ('agent-mcp-readme-cli-' + [Guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Path $runRoot | Out-Null +$agent = Join-Path $repository 'build\Release\Agent.App\net10.0-windows10.0.26100.0\agent-mcp.exe' +$windowsTests = Join-Path $repository 'build\Release\Agent.Windows.Tests\net10.0-windows10.0.26100.0\Agent.Windows.Tests.exe' +$configPath = Join-Path $runRoot 'agent.yaml' +$logPath = Join-Path $evidence 'verification.log' +$failed = $false +$aliases = @( + ('readme-python-' + [Guid]::NewGuid().ToString('N').Substring(0, 8)) + ('readme-powershell-' + [Guid]::NewGuid().ToString('N').Substring(0, 8)) + ('readme-cmd-' + [Guid]::NewGuid().ToString('N').Substring(0, 8)) +) +$pythonAlias, $powershellAlias, $cmdAlias = $aliases +$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0) +$listener.Start() +$httpPort = ([Net.IPEndPoint]$listener.LocalEndpoint).Port +$listener.Stop() +$resolutionStorePath = $null +$server = $null + +function Invoke-Step { + param([string]$Name, [scriptblock]$Action) + "START $Name $(Get-Date -Format o)" | Tee-Object -FilePath $logPath -Append + try { + $global:LASTEXITCODE = 0 + & $Action *>&1 | Tee-Object -FilePath $logPath -Append + if ($LASTEXITCODE -ne 0) { throw "$Name exited with $LASTEXITCODE" } + "PASS $Name $(Get-Date -Format o)" | Tee-Object -FilePath $logPath -Append + } + catch { + $_ | Out-String | Tee-Object -FilePath $logPath -Append + $script:failed = $true + "FAIL $Name $(Get-Date -Format o)" | Tee-Object -FilePath $logPath -Append + } +} + +function Invoke-ExpectedFailure { + param([string]$Name, [string[]]$Arguments) + "START $Name $(Get-Date -Format o)" | Tee-Object -FilePath $logPath -Append + & $agent @Arguments *>&1 | Tee-Object -FilePath $logPath -Append + if ($LASTEXITCODE -ne 2) { + $script:failed = $true + "FAIL $Name expected_exit=2 actual_exit=$LASTEXITCODE" | Tee-Object -FilePath $logPath -Append + } + else { "PASS $Name expected_exit=2" | Tee-Object -FilePath $logPath -Append } +} + +function Start-And-ProbeServer { + param([bool]$ExplicitServe) + $arguments = if ($ExplicitServe) { + @('serve', '--config', $configPath, '--lang', 'ja', '--transport', 'http') + } else { + @('--config', $configPath, '--lang', 'ja', '--transport', 'http') + } + $script:server = Start-Process -FilePath $agent -ArgumentList $arguments -WorkingDirectory (Split-Path $agent) -WindowStyle Hidden -PassThru + $endpoint = "http://127.0.0.1:$httpPort/mcp" + for ($attempt = 0; $attempt -lt 100; $attempt++) { + if ($script:server.HasExited) { throw "Server exited before becoming ready: $($script:server.ExitCode)" } + try { + $response = Invoke-WebRequest -Uri $endpoint -Method Get -SkipHttpErrorCheck + if ($response.StatusCode -eq 405) { return } + } catch { } + Start-Sleep -Milliseconds 100 + } + throw 'Server did not become ready.' +} + +try { + $forwardRoot = $testRootPath.Replace('\', '/') + $forwardPrompt = (Join-Path $repository 'prompts\user-system.md').Replace('\', '/') + $forwardSkills = (Join-Path $repository 'skills').Replace('\', '/') + $forwardLocales = (Join-Path $repository 'config\locales').Replace('\', '/') + $forwardLogs = (Join-Path $runRoot 'logs').Replace('\', '/') + @" +schema_version: 1 +application: + language: en +mcp: + transport: http + http: + host: 127.0.0.1 + port: $httpPort + images: + max_count: 4 + max_bytes_each: 10485760 + max_total_bytes: 20971520 + tasks: + time_to_live_hours: 24 + poll_interval_ms: 1000 +agent: + user_system_prompt: $forwardPrompt + max_iterations: 8 + same_failure_limit: 2 +ollama: + endpoint: http://127.0.0.1:11434 + model: deepseek-v4-flash:cloud + think: true + keep_alive: 5m + options: + num_ctx: 32768 + temperature: 0 + extra_options: {} +planner: + max_tasks: 8 +sandbox: + profile: development + workspaces: + - $forwardRoot + mounts: [] + execution: + executables: + - $pythonAlias + shells: + - $powershellAlias + - $cmdAlias + network: + access: none + local_endpoints: [] + restricted_endpoints: [] + resources: + profile: constrained +file_edit: + memory_backup_limit_mb: 16 +limits: + whole_read_bytes: 1048576 + search_max_results: 1000 + tail_max_lines: 10000 + context_summary_ratio: 0.8 +localization: + directory: $forwardLocales + fallback: en +skills: + directory: $forwardSkills +logging: + directory: $forwardLogs + level: information + http_requests: true + max_file_bytes: 10485760 + retained_files: 5 +"@ | Set-Content -LiteralPath $configPath -Encoding utf8NoBOM + + @( + "timestamp=$(Get-Date -Format o)", + "commit=$(git -C $repository rev-parse HEAD)", + "administrator=$administrator", + "os=$([Environment]::OSVersion.VersionString)", + "venv_python=$venvPythonPath", + "venv_version=$(& $venvPythonPath --version)", + "powershell=$pwshPath", + "cmd=$cmdPath" + ) | Set-Content -LiteralPath (Join-Path $evidence 'metadata.txt') -Encoding utf8NoBOM + + Invoke-Step 'cli-sandbox-resolve' { & $agent sandbox resolve --config $configPath --lang ja } + Invoke-Step 'cli-sandbox-confirm-yes' { & $agent sandbox confirm --yes --config $configPath --lang ja } + $normalizedConfig = [IO.Path]::GetFullPath($configPath).ToUpperInvariant() + $hash = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($normalizedConfig))) + $resolutionStorePath = Join-Path $env:LOCALAPPDATA "AgentSandboxMcp\environment-resolutions\$hash.json" + Invoke-Step 'cli-executable-resolve-path' { & $agent executable resolve $pythonAlias --path $basePythonPath --config $configPath --lang ja } + Invoke-Step 'cli-executable-approve-runtime-root-yes' { & $agent executable approve $pythonAlias --path $basePythonPath --runtime-root $pythonRuntime --yes --config $configPath --lang ja } + Invoke-Step 'cli-shell-resolve-powershell' { & $agent executable resolve $powershellAlias --path $pwshPath --config $configPath --lang en } + Invoke-Step 'cli-shell-approve-runtime-root-yes' { & $agent executable approve $powershellAlias --path $pwshPath --runtime-root $pwshRuntime --yes --config $configPath --lang en } + Invoke-Step 'cli-shell-resolve-cmd' { & $agent executable resolve $cmdAlias --path $cmdPath --config $configPath --lang en } + Invoke-Step 'cli-shell-approve-cmd-no-runtime' { & $agent executable approve $cmdAlias --path $cmdPath --no-runtime --yes --config $configPath --lang en } + Invoke-Step 'cli-executable-list' { & $agent executable list --config $configPath --lang ja } + Invoke-Step 'cli-sandbox-show-text-default-format' { & $agent sandbox show --config $configPath --lang en } + Invoke-Step 'cli-sandbox-show-json' { & $agent sandbox show --format json --config $configPath --lang ja } + if ($CliOnly) { + Invoke-ExpectedFailure 'cli-check-detects-non-admin' @('check', '--config', $configPath, '--lang', 'ja') + } + else { + Invoke-Step 'cli-check' { & $agent check --config $configPath --lang ja } + } + + Invoke-ExpectedFailure 'cli-invalid-transport' @('serve', '--config', $configPath, '--transport', 'invalid') + Invoke-ExpectedFailure 'cli-invalid-format' @('sandbox', 'show', '--format', 'xml', '--config', $configPath) + Invoke-ExpectedFailure 'cli-management-missing-config' @('sandbox', 'resolve') + Invoke-ExpectedFailure 'cli-mutually-exclusive-runtime-options' @('executable', 'approve', $pythonAlias, '--path', $basePythonPath, '--runtime-root', $pythonRuntime, '--no-runtime', '--config', $configPath) + + if (-not $CliOnly) { + Invoke-Step 'cli-serve-explicit-http' { + Start-And-ProbeServer $true + $script:server.Kill($true) + $script:server.WaitForExit() + $script:server = $null + } + Invoke-Step 'cli-serve-implicit-option-first' { + Start-And-ProbeServer $false + $script:server.Kill($true) + $script:server.WaitForExit() + $script:server = $null + } + + $env:AGENT_MCP_MONKEY_ROOT = $testRootPath + $env:AGENT_MCP_TEST_VENV_PYTHON = $venvPythonPath + $env:AGENT_MCP_TEST_POWERSHELL = $pwshPath + $env:AGENT_MCP_TEST_POWERSHELL_RUNTIME = $pwshRuntime + $env:AGENT_MCP_TEST_CMD = $cmdPath + Invoke-Step 'readme-operational-windows-tests' { + & $windowsTests -class Agent.Windows.Tests.ReadmeOperationalTests -parallel none -failSkips + } + } + Invoke-Step 'cli-executable-revoke-python' { & $agent executable revoke $pythonAlias --yes --config $configPath --lang ja } + Invoke-Step 'cli-executable-revoke-powershell' { & $agent executable revoke $powershellAlias --yes --config $configPath --lang en } + Invoke-Step 'cli-executable-revoke-cmd' { & $agent executable revoke $cmdAlias --yes --config $configPath --lang en } +} +finally { + if ($server -and -not $server.HasExited) { $server.Kill($true); $server.WaitForExit() } + foreach ($alias in $aliases) { + & $agent executable revoke $alias --yes --config $configPath --lang en 2>$null | Out-Null + } + if ($resolutionStorePath -and (Test-Path -LiteralPath $resolutionStorePath)) { Remove-Item -LiteralPath $resolutionStorePath -Force } + if (Test-Path -LiteralPath $runRoot) { + $resolvedRunRoot = (Resolve-Path -LiteralPath $runRoot).Path + if (-not $resolvedRunRoot.StartsWith($testRootPath.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase)) { + throw 'Refusing to clean a test directory outside TestRoot.' + } + Remove-Item -LiteralPath $resolvedRunRoot -Recurse -Force + } + Remove-Item Env:AGENT_MCP_MONKEY_ROOT,Env:AGENT_MCP_TEST_VENV_PYTHON,Env:AGENT_MCP_TEST_POWERSHELL,Env:AGENT_MCP_TEST_POWERSHELL_RUNTIME,Env:AGENT_MCP_TEST_CMD -ErrorAction SilentlyContinue +} + +if ($failed) { throw "README verification failed. Evidence: $evidence" } +Write-Output "README verification PASS. Evidence: $evidence" |
