Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,234 @@ jobs:
- name: Build and verify portable Desktop shell
shell: pwsh
run: ./scripts/build-desktop.ps1 -SkipFrontend -VerifyReproducible

ui-evidence-windows:
name: Real Edge UI evidence (standard user, Windows 2022)
runs-on: windows-2022
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Set up current Go 1.25 patch
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.25.x'
check-latest: true
cache: true
- name: Verify real Edge UI evidence matrix and regression detection
shell: pwsh
run: |
$artifactDirectory = Join-Path $env:RUNNER_TEMP "ui-evidence-smoke"
New-Item -ItemType Directory -Path $artifactDirectory -Force | Out-Null
if ($env:GITHUB_RUN_ID -notmatch '^\d+$' -or $env:GITHUB_RUN_ATTEMPT -notmatch '^\d+$') {
throw "GitHub run identity is unavailable"
}
# Hosted Windows RUNNER_TEMP is reparse-backed, and runneradmin's
# LOCALAPPDATA is not traversable by the disposable standard user.
# Use one exact child of the direct system volume root, then grant
# that child (never the root) only to the temporary SID below.
if ([string]::IsNullOrWhiteSpace($env:SystemRoot) -or
-not [System.IO.Path]::IsPathFullyQualified($env:SystemRoot)) {
throw "Windows system root is unavailable"
}
$directTempParent = [System.IO.Path]::GetPathRoot(
[System.IO.Path]::GetFullPath($env:SystemRoot))
$directTempParentItem = Get-Item -LiteralPath $directTempParent -Force
if (-not $directTempParentItem.PSIsContainer -or
($directTempParentItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
throw "Windows system volume root is indirect"
}
$directTempName = "cyberagent-ui-evidence-$($env:GITHUB_RUN_ID)-$($env:GITHUB_RUN_ATTEMPT)"
$directTempRoot = Join-Path $directTempParent $directTempName
$resolvedTempParent = [System.IO.Path]::GetFullPath((Split-Path -Parent $directTempRoot))
if (-not [string]::Equals(
$resolvedTempParent.TrimEnd('\'),
$directTempParent.TrimEnd('\'),
[System.StringComparison]::OrdinalIgnoreCase)) {
throw "UI evidence direct temp root escaped the system volume root"
}
if (Test-Path -LiteralPath $directTempRoot) {
throw "UI evidence direct temp root already exists"
}
$directTempItem = New-Item -ItemType Directory -Path $directTempRoot
if (($directTempItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
throw "UI evidence direct temp root is indirect"
}
$directArtifactDirectory = Join-Path $directTempRoot "artifacts"
$testBinary = Join-Path $directTempRoot "browserruntime.test.exe"
$userHome = Join-Path $directTempRoot "home"
$userLocalAppData = Join-Path $userHome "AppData\Local"
$userRoamingAppData = Join-Path $userHome "AppData\Roaming"
foreach ($directory in @($directArtifactDirectory, $userLocalAppData, $userRoamingAppData)) {
New-Item -ItemType Directory -Path $directory -Force | Out-Null
}

$runSuffix = $env:GITHUB_RUN_ID.Substring(
[Math]::Max(0, $env:GITHUB_RUN_ID.Length - 8))
$standardUserName = "cyberui$runSuffix$($env:GITHUB_RUN_ATTEMPT)"
if ($standardUserName -notmatch '^cyberui\d{1,12}$' -or
$standardUserName.Length -gt 20) {
throw "derived UI evidence standard-user name is invalid"
}
if (Get-LocalUser -Name $standardUserName -ErrorAction SilentlyContinue) {
throw "UI evidence standard user already exists"
}
$standardUserCreated = $false
$workspaceGrantAdded = $false
$standardUserSID = $null
$process = $null
$securePassword = $null
$cleanupFailure = $null
try {
go test -c -o $testBinary ./internal/browserruntime
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $testBinary)) {
throw "compile real Edge UI evidence test binary failed"
}

$plainPassword = "Aa1!$([guid]::NewGuid().ToString('N'))zZ9!"
$securePassword = ConvertTo-SecureString $plainPassword -AsPlainText -Force
New-LocalUser -Name $standardUserName -Password $securePassword `
-AccountNeverExpires -PasswordNeverExpires -UserMayNotChangePassword | Out-Null
$standardUserCreated = $true
$usersGroup = Get-LocalGroup -SID "S-1-5-32-545"
Add-LocalGroupMember -Group $usersGroup -Member $standardUserName
$standardUserSID = (Get-LocalUser -Name $standardUserName).SID.Value
if ($standardUserSID -notmatch '^S-1-5-21-(\d+-){3}\d+$') {
throw "UI evidence standard-user SID is invalid"
}

& icacls.exe $env:GITHUB_WORKSPACE /grant:r `
("*{0}:(OI)(CI)RX" -f $standardUserSID) /Q | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "grant standard-user workspace read access failed"
}
$workspaceGrantAdded = $true
& icacls.exe $directTempRoot /grant:r `
("*{0}:(OI)(CI)M" -f $standardUserSID) /Q | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "grant standard-user temporary-root access failed"
}

$start = [System.Diagnostics.ProcessStartInfo]::new()
$start.FileName = $testBinary
$start.WorkingDirectory = $env:GITHUB_WORKSPACE
$start.UseShellExecute = $false
$start.CreateNoWindow = $true
$start.RedirectStandardOutput = $true
$start.RedirectStandardError = $true
$start.Domain = $env:COMPUTERNAME
$start.UserName = $standardUserName
$start.Password = $securePassword
# Edge requires the disposable account's HKCU hive during browser
# initialization. The exact profile is removed by SID below.
$start.LoadUserProfile = $true
$start.ArgumentList.Add("-test.v")
$start.ArgumentList.Add("-test.timeout=3m")
$start.ArgumentList.Add(
"-test.run=^TestInstalledEdgeUIEvidenceHeadlessMatrixAndRegression$")
$start.Environment.Clear()
$processEnvironment = [ordered]@{
APPDATA = $userRoamingAppData
CYBERAGENT_UI_EVIDENCE_ARTIFACT_DIR = $directArtifactDirectory
CYBERAGENT_UI_EVIDENCE_SMOKE = "1"
GITHUB_ACTIONS = "true"
GIT_CONFIG_COUNT = "1"
GIT_CONFIG_GLOBAL = "NUL"
GIT_CONFIG_KEY_0 = "safe.directory"
GIT_CONFIG_NOSYSTEM = "1"
GIT_CONFIG_VALUE_0 = $env:GITHUB_WORKSPACE
GIT_OPTIONAL_LOCKS = "0"
GIT_TERMINAL_PROMPT = "0"
HOME = $userHome
LOCALAPPDATA = $userLocalAppData
PATH = "C:\Program Files\Git\cmd;$env:SystemRoot\System32;$env:SystemRoot"
PATHEXT = ".COM;.EXE;.BAT;.CMD"
SystemRoot = $env:SystemRoot
TEMP = $directTempRoot
TMP = $directTempRoot
USERPROFILE = $userHome
WINDIR = $env:WINDIR
}
foreach ($entry in $processEnvironment.GetEnumerator()) {
$start.Environment[$entry.Key] = $entry.Value
}

$process = [System.Diagnostics.Process]::new()
$process.StartInfo = $start
if (-not $process.Start()) {
throw "start real Edge UI evidence as a standard user failed"
}
$plainPassword = $null
$stdout = $process.StandardOutput.ReadToEndAsync()
$stderr = $process.StandardError.ReadToEndAsync()
if (-not $process.WaitForExit(240000)) {
$process.Kill($true)
$process.WaitForExit()
throw "real Edge UI evidence standard-user process timed out"
}
$stdout.Result | Write-Host
$stderr.Result | Write-Host

if (Test-Path -LiteralPath $directArtifactDirectory) {
Get-ChildItem -LiteralPath $directArtifactDirectory -File | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $artifactDirectory -Force
}
}
if ($process.ExitCode -ne 0) {
throw "real Edge UI evidence verification failed with exit code $($process.ExitCode)"
}
if (-not (Test-Path -LiteralPath (Join-Path $artifactDirectory "receipt.json"))) {
throw "real Edge UI evidence receipt was not produced"
}
}
finally {
if ($null -ne $process) {
$process.Dispose()
}
if ($null -ne $securePassword) {
$securePassword.Dispose()
}
if ($workspaceGrantAdded -and $null -ne $standardUserSID) {
& icacls.exe $env:GITHUB_WORKSPACE /remove:g `
("*{0}" -f $standardUserSID) /Q | Out-Host
if ($LASTEXITCODE -ne 0) {
$cleanupFailure = "remove standard-user workspace grant failed"
}
}
if ($standardUserCreated) {
$profileDeadline = (Get-Date).AddSeconds(5)
do {
$loadedProfile = Get-CimInstance -ClassName Win32_UserProfile |
Where-Object { $_.SID -eq $standardUserSID }
if ($null -eq $loadedProfile -or -not $loadedProfile.Loaded) {
break
}
Start-Sleep -Milliseconds 250
} while ((Get-Date) -lt $profileDeadline)
if ($null -ne $loadedProfile) {
if ($loadedProfile.Special -or $loadedProfile.Loaded) {
$cleanupFailure = "UI evidence standard-user profile is not removable"
}
else {
$loadedProfile | Remove-CimInstance
}
}
Remove-LocalUser -Name $standardUserName
}
if (Test-Path -LiteralPath $directTempRoot) {
Remove-Item -LiteralPath $directTempRoot -Recurse -Force
}
if (Test-Path -LiteralPath $directTempRoot) {
throw "UI evidence direct temp root was not cleaned"
}
if ($null -ne $cleanupFailure) {
throw $cleanupFailure
}
}
- name: Upload real Edge UI evidence receipt
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ui-evidence-edge-smoke
path: ${{ runner.temp }}/ui-evidence-smoke
if-no-files-found: warn
retention-days: 5
10 changes: 8 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ A child reaches `ready_for_review` only with a clean worktree, a HEAD descending

By default the only executable check is `git diff --check`, which does not run repository code. Because `go_test` and `npm_test` execute child-authored code on the host, the relevant control capability must be enabled and the current Run must still be running with `full_access` (or the explicitly higher `debug` mode); Desktop also requires explicit `--enable-batch-delivery-control`, while host validation additionally requires permission control, danger-full-access, and `--enable-batch-validation-execution`. Validation uses a Windows Job Object or Unix process-group lifecycle boundary, bypasses the Go test cache, and persists only complete-stream output digests. The stripped/offline environment still is not an OS network or filesystem sandbox, and deliberate POSIX daemonization outside the inherited process group remains an explicit host-execution residual. See [Deliverable Multi-Agent Batches](docs/batch-delivery.md) and [ADR 0119](docs/adr/0119-deliverable-batch-agents.md).

### Source-bound real-browser UI evidence

Schema v119 `ui-evidence.v1` binds real-page verification to the commit/dirty digest/index/worktree manifest, exact build/start recipes, fixed browser version and executable SHA-256, literal loopback URL/route, viewport/DPR, locale/theme/reduced motion, deterministic fixture/seed/page state, steps, and capture policy. Application revalidates source before build, after readiness, after browser assertions, and again after owned-process cleanup before terminal completion. It refuses an occupied port and never adopts an existing service or personal browser Profile. Windows Desktop execution is off by default and appears only when Run execution, `full_access`, danger-full-access, restricted CDP, and `--enable-ui-evidence` all hold.

Desktop, authenticated OpenAPI, and the read/export-only CLI share immutable Attempt, step, and artifact semantics. PNG, DOM, accessibility, console/page-error, network/HTTP, and performance evidence retain SHA-256, MIME, dimensions, viewport, source step/commit, Run/Attempt, redaction provenance, and the retention policy; PNG dimensions must match `viewport × DPR`. Page content and artifacts remain untrusted and non-authorizing. `not_run` is always neutral; only exact `passed` is success. Windows CI runs real Edge in a creation-time Job Object and temporary Profile across desktop/mobile, theme/locale/reduced-motion cells, and proves a missing click handler is detected only by a real-page interaction assertion. See the [UI Evidence guide](docs/ui-evidence.md) and [ADR 0120](docs/adr/0120-source-bound-real-browser-ui-evidence.md).

### Real Git, PowerShell, and Bash

Prayu invokes real Git and operating-system shells; it is not a command emulator. It deliberately does not give the model a permanent, unreviewed raw terminal. The Code workflow separates execution by risk:
Expand All @@ -119,7 +125,7 @@ Every `debug_terminal` write still passes Shell Policy; commands that require se
- Conservative commands use Go-owned fixed templates. PowerShell/Bash is available only through one of three independent paths: the Code/Deliver/root + `full_access` Run-owned runtime, per-command approval, or a revocable Debug lease. General host execution and Debug authority cannot be enabled by a model, Skill, or repository document.
- The Docker Sandbox product entry is disabled by default. An explicit process capability, the current `docker` Profile, a matching permission tier, an exact per-call approval, Policy, budgets, and a 30-second readiness check must all hold at once; database records can never restore start authority after a restart.
- Product execution currently accepts only environment-free, secret-free `network=disabled` Manifests and pins `network none` on both the Docker create and inspect sides. Allowlist/scoped egress still lacks a Go-owned host/port/protocol guard, so it always fails closed with `managed_egress_unavailable`; there is no host fallback when Docker is unavailable.
- The built-in browser has no product entry point yet. A restricted runtime core exists, but independent OS/container network-containment evidence is incomplete.
- Windows Desktop exposes loopback-only real-browser evidence only when explicit `--enable-ui-evidence` and its Run-execution/danger-full-access/restricted-CDP prerequisites all hold. macOS and the ordinary CLI remain read-only; Full CDP is still a separate, default-off Debug authority surface.
- Windows/macOS Desktop are currently unsigned developer/operator portable previews, not released installers; the macOS artifact is only ad-hoc signed and not notarized.

### Docker Sandbox product entry (disabled by default)
Expand Down Expand Up @@ -277,7 +283,7 @@ At **2026-08-13 / schema v96 / P13-H1 through P13-H3**, the old task book estima
| P6-P8 | Sandbox evidence contracts, Skill Registry, Finding/Evidence/Report, SARIF, and CI projection |
| P9 / Desktop D0-D1 | HTTP/OpenAPI, React/TUI/Desktop, repository/diff/editor/verification/Handoff, and liquid-glass workbench |
| P10-A through P10-M | Go/Rust Analyzer protocol, vectors, embedded WASI execution, one-shot capability, and product integration |
| P11-A through P11-C | Browser permissions, Profiles, CDP, and WFP evidence; product entry remains closed |
| P11-A through P11-C / schema v119 | Browser permissions, Profiles, CDP/WFP evidence, and the gated source-bound UI-evidence product path |
| P12-A through P12-E | Interaction models, controlled Windows Runner, user terminal, four permission tiers, command approval, and host-execution ledger |
| P13-A through P13-H | Run Activity, public model stream, continuous chat, Markdown, diff review, Live Activity, and desktop visual consolidation |

Expand Down
Loading