summaryrefslogtreecommitdiff
path: root/scripts/Verify-Readme.ps1
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-13 19:15:01 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-13 19:15:01 +0900
commit36ae1a4cba791ec633a81b38b1d8345af8487276 (patch)
tree91cef96efac7eb8c2754658ce5af5181b1a15898 /scripts/Verify-Readme.ps1
parent76fdcb43585a4c4d288c1026d318a0ddbe3e26ab (diff)
feat!: unify sandbox configuration and permission application
Replace the split executable approval and sandbox resolution workflows with schema_version 2 and a YAML-driven execution.entries model. Resolve workspace, mount, executable, runtime, network, and resource settings into one config-scoped Applied Configuration snapshot protected by a user-only DACL and written with atomic replacement. Reduce the public CLI to implicit server startup, show, and apply. Remove serve/check, executable and sandbox subcommands, and configuration override flags including --config, --lang, --transport, --path, and --runtime-root. Add migration diagnostics for every removed schema and command surface, and keep server preflight mandatory for both stdio and HTTP. Update effective-policy compilation to validate the applied fingerprint, resolved paths, and executable hashes before startup. Preserve fail-closed behavior while allowing execution entries and runtime roots inside a development workspace to inherit that workspace capability without creating an impossible narrower ACL boundary. Rewrite the README around the consolidated permission matrix and document configuration, application, store location, startup, transport-specific elevation, Python 3.14, PowerShell, and cmd usage. Align specifications, design, implementation plan, traceability, audit report, localization, publish scripts, and operational verification with the breaking schema. Verification: Release build completed with zero warnings and errors; Agent.Core.Tests 104/104 and Agent.Planner.Tests 10/10 passed; administrator Windows Security/Recovery 56/56 and README operational 19/19 passed with no skips; published HTTP MCP completed a cmd/curl/venv-Python task with network deny and zero ACL, WFP, journal, backup, or process residue.
Diffstat (limited to 'scripts/Verify-Readme.ps1')
-rw-r--r--scripts/Verify-Readme.ps1261
1 files changed, 67 insertions, 194 deletions
diff --git a/scripts/Verify-Readme.ps1 b/scripts/Verify-Readme.ps1
index f67f640..42c8b8f 100644
--- a/scripts/Verify-Readme.ps1
+++ b/scripts/Verify-Readme.ps1
@@ -1,255 +1,128 @@
param(
- [Parameter(Mandatory)]
- [string]$TestRoot,
- [Parameter(Mandatory)]
- [string]$VenvPython,
+ [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.' }
+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 operational verification must run as Windows Administrator.' }
$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" }
+$pythonRuntime = (((Get-Content -LiteralPath (Join-Path $venvRoot 'pyvenv.cfg')) | Where-Object { $_ -match '^home\s*=' }) -split '=', 2)[1].Trim()
$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'))
+$runRoot = Join-Path $testRootPath ('agent-mcp-readme-' + [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
+$failed = $false
-function Invoke-Step {
- param([string]$Name, [scriptblock]$Action)
+function Invoke-Step([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
- }
+ "PASS $Name" | Tee-Object -FilePath $logPath -Append
+ } catch { $script:failed = $true; "FAIL $Name $($_.Exception.Message)" | Tee-Object -FilePath $logPath -Append }
}
-function Invoke-ExpectedFailure {
- param([string]$Name, [string[]]$Arguments)
- "START $Name $(Get-Date -Format o)" | Tee-Object -FilePath $logPath -Append
+function Invoke-ExpectedFailure([string]$Name, [string[]]$Arguments) {
& $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.'
+ if ($LASTEXITCODE -ne 2) { $script:failed = $true; "FAIL $Name expected=2 actual=$LASTEXITCODE" | Tee-Object -FilePath $logPath -Append }
+ else { "PASS $Name" | Tee-Object -FilePath $logPath -Append }
}
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('\', '/')
+ $listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0); $listener.Start()
+ $httpPort = ([Net.IPEndPoint]$listener.LocalEndpoint).Port; $listener.Stop()
+ $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('\','/')
+ $pythonYaml = $basePythonPath.Replace('\','/'); $runtimeYaml = $pythonRuntime.Replace('\','/')
+ $pwshYaml = $pwshPath.Replace('\','/'); $cmdYaml = $cmdPath.Replace('\','/')
@"
-schema_version: 1
-application:
- language: en
+schema_version: 2
+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
+ http: { host: 127.0.0.1, port: $httpPort }
+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 }
+planner: { max_tasks: 8 }
sandbox:
profile: development
- workspaces:
- - $forwardRoot
+ 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
+ entries:
+ - { alias: python, path: $pythonYaml, runtime_root: $runtimeYaml }
+ - { alias: powershell, path: '$pwshYaml' }
+ - { alias: cmd, path: $cmdYaml }
+ network: { access: none, local_endpoints: [], restricted_endpoints: [] }
+ resources: { profile: constrained }
+localization: { directory: $forwardLocales, fallback: en }
+skills: { directory: $forwardSkills }
+logging: { directory: $forwardLogs, level: information, 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 }
- }
+ $appliedStorePath = Join-Path $env:LOCALAPPDATA "AgentSandboxMcp\applied-configurations\$hash.json"
- 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)
+ Invoke-Step 'cli-show-before-apply' { & $agent show $configPath }
+ Invoke-Step 'cli-apply-yes' { & $agent apply $configPath --yes }
+ Invoke-Step 'cli-show-applied' { & $agent show $configPath }
+ Invoke-ExpectedFailure 'removed-serve' @('serve')
+ Invoke-ExpectedFailure 'removed-check' @('check')
+ Invoke-ExpectedFailure 'removed-config-option' @('--config', $configPath)
+ Invoke-ExpectedFailure 'removed-language-option' @('--lang', 'ja')
+ Invoke-ExpectedFailure 'removed-transport-option' @('--transport', 'http')
+ Invoke-ExpectedFailure 'removed-executable-command' @('executable', 'list')
+ Invoke-ExpectedFailure 'removed-sandbox-command' @('sandbox', 'show')
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
+ Invoke-Step 'implicit-http-start' {
+ $script:server = Start-Process -FilePath $agent -ArgumentList @($configPath) -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: $($script:server.ExitCode)" }
+ try { $response = Invoke-WebRequest -Uri $endpoint -Method Get -SkipHttpErrorCheck; if ($response.StatusCode -eq 405) { break } } catch { }
+ Start-Sleep -Milliseconds 100
+ }
+ if ($attempt -eq 100) { throw 'Server did not become ready.' }
+ $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_POWERSHELL_RUNTIME = Split-Path $pwshPath
$env:AGENT_MCP_TEST_CMD = $cmdPath
- Invoke-Step 'readme-operational-windows-tests' {
- & $windowsTests -class Agent.Windows.Tests.ReadmeOperationalTests -parallel none -failSkips
- }
+ 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 {
+} 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 ($appliedStorePath -and (Test-Path -LiteralPath $appliedStorePath)) { Remove-Item -LiteralPath $appliedStorePath -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
+ $resolved = (Resolve-Path -LiteralPath $runRoot).Path
+ if (-not $resolved.StartsWith($testRootPath.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase)) { throw 'Refusing to clean outside TestRoot.' }
+ Remove-Item -LiteralPath $resolved -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
}