diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index cc8637c..726c2bd 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "anode", "displayName": "Anode background Windows desktop", - "version": "0.11.1", + "version": "0.11.2", "description": "Background Windows desktop for AI agents: native GUI automation, screenshots and app testing.", "author": { "name": "skulitom", diff --git a/.github/RELEASE_NOTES.md b/.github/RELEASE_NOTES.md index 35625d2..883dca6 100644 --- a/.github/RELEASE_NOTES.md +++ b/.github/RELEASE_NOTES.md @@ -1,29 +1,49 @@ -## What's new in 0.11.1 +## What's new in 0.11.2 -This patch fixes issues found after 0.11.0 and adds live Windows validation of the merged changes. +This patch fixes bugs found after 0.11.1, most of them by AI agents using Anode every day. -- **Safer Android automation.** adb commands cannot change the shared server or replace/remove - another device's forwarded local port, including through repeated `wait-for-*` prefixes. Emulator - boot probes stay within the caller's reply deadline, even when adb hangs. -- **Reliable display recovery.** The viewer remembers the display Windows confirmed. A lost reply - is never replayed or treated as success, and lease cleanup restores a change that finishes late. -- **Recovery after an agent is killed.** A generated MCP session's desktop lease ends when its - process is gone and any admitted action finishes. Stable `ANODE_AGENT_ID` leases retain their - existing release-or-expiry behavior. -- **Older MCP audio clients work.** Clients using protocol `2024-11-05` receive recordings as WAV - resources; newer clients continue to receive native audio blocks. -- **MCP Registry publication.** Anode's initial metadata listing is live. Release CI now builds and - attests the optional MCPB bundle for owner testing; it remains a workflow artifact until its - separate Claude Desktop validation passes. +- **Typing keeps pace with the program.** `seat_type` and `anode type` sent characters as fast as + Windows took them, so a busy program, such as Chrome editing a long field, could drop and reorder + them while the call reported success. Characters now go out 15 ms apart by default, and typing + pauses while the program is busy; `perCharMs` sets the pace. A call that stops partway reports + exactly how many characters were typed. +- **`set_value` checks that the value took.** On a Chrome drop-down list (`` fails instead of reporting success; the open list's observation lists each control once | it prints `"passed": true`; a warning that the open list's options weren't listed is a finding for a later release, not a failure | + | `test-development.ps1` | typing's new pace, with mouse, key and text delivery checked in a headed Chrome page | it ends without an error | + | File Explorer | `inspect` on an Explorer window, which failed in 0.11.1 | it lists the window's controls | + +4. **Release.** If every check passed, tag the candidate and push the tag from the candidate + checkout, whose `HEAD` is the candidate ([release process](../CONTRIBUTING.md#release-process), + step 6): + + ```powershell + git tag v0.11.2 HEAD + git push origin v0.11.2 + ``` + + The Release workflow builds, tests and publishes the release. After it: the package manifests + (step 7), and the Claude Desktop test of the workflow's MCPB bundle + ([release gate](../packaging/mcpb/README.md#claude-desktop-test-release-gate)), which lets the + registry list 0.11.2 as installable. The validation results go into this record then. + +### Covered without a live run + +The seat profile held on the other desktop (Restart Manager), requests queued behind a long +operation, packaged apps' folders and the uninstaller's list are covered by quick checks with +stand-ins and by CI's disposable installs, not by a live seat. Startup timings are in +[startup performance](STARTUP-PERFORMANCE.md). + +### Known and open, for a later release + +- A Chrome window's first observation can come back without the page, and page controls sit below + the default `maxDepth` of 8. Observe again with `maxDepth` 16. +- On Windows' "Open File - Security Warning" dialog, `invoke` on Run reports success, but nothing + runs. Focus the dialog with `seat_window`, then click. +- `seat_window` `raise` can leave a UWP window behind the foreground window. Use `focus`. +- `anode type` has no option for a slower pace (MCP's `seat_type` has `perCharMs`). +- `anode.log` is never rotated ([privacy](PRIVACY.md)). + +## 0.11.1 release validation — 2 October 2026 **Scope: 0.11.1 release validation.** All open PRs were reviewed, corrected where necessary and merged after their Windows CI passed. The patch includes legacy MCP audio compatibility, shared @@ -6,7 +125,7 @@ adb server/forward protections, bounded emulator startup, confirmed display memo of leases owned by killed MCP processes. Publication remains gated by Windows CI and the Release workflow. -## Live validation — 2 October 2026 +### Live validation — 2 October 2026 The self-contained 0.11.1 candidate daemon and seat host ran together on Windows 11 Pro build 26200 in the separate `dev` channel, child session 3, initially 1280x720 at 100%. The user made @@ -31,7 +150,7 @@ Local logs, JSON results, screenshots, audio and the MCP integration harness are `artifacts/validation-0.11.1` (ignored by Git). The separate PR review record is under `artifacts/pr-review-2026-10-02`. -## Automated validation +### Automated validation - The candidate passed all **83 quick self-tests** and documentation checks. The environment self-test (`selftest --no-gamepad`) passed **86 checks**, adding screen capture, geometry and @@ -48,7 +167,7 @@ Local logs, JSON results, screenshots, audio and the MCP integration harness are - The .NET 10 runtime pin remains **10.0.12**, verified against Microsoft's release metadata on 2 October 2026. The package includes that runtime. -## Remaining coverage limits +### Remaining coverage limits - The live display reconnect fallback was not needed: every requested display applied live. Credential-dialog seats refuse reconnects that would need the password again. Failed, clamped, @@ -69,7 +188,7 @@ Local logs, JSON results, screenshots, audio and the MCP integration harness are Authenticode signature. No clean-machine Windows compatibility certification, long-running soak test or comprehensive security audit is claimed. -## Live validation — 28-29 September 2026 +### Live validation — 28-29 September 2026 The earlier 0.11.0 evidence, including Android Studio, seat-browser profile checks and diagnosis of the damaged Pixel 3a image, is preserved in the diff --git a/packaging/mcpb/manifest.json b/packaging/mcpb/manifest.json index b306b42..4b19a0c 100644 --- a/packaging/mcpb/manifest.json +++ b/packaging/mcpb/manifest.json @@ -2,7 +2,7 @@ "manifest_version": "0.3", "name": "anode", "display_name": "Anode background Windows desktop", - "version": "0.11.1", + "version": "0.11.2", "description": "Background Windows desktop for AI agents: native GUI automation, screenshots and app testing.", "long_description": "Runs apps in a Windows child session with its own screen, pointer and keyboard focus, so an agent can automate native UI, take screenshots and run headed app/browser tests while you keep working. Requires 64-bit Windows 10/11 Pro, Enterprise or Education (or Windows Server with an RDP host) and a one-time `anode setup` as administrator. Shares your files, account and network; it is not a security sandbox.", "author": { diff --git a/scripts/test-chrome-form.ps1 b/scripts/test-chrome-form.ps1 new file mode 100644 index 0000000..1375030 --- /dev/null +++ b/scripts/test-chrome-form.ps1 @@ -0,0 +1,183 @@ +#requires -Version 5.1 +<# +.SYNOPSIS +Tests set_value and observation on a Chrome form in an already running Anode seat. +Uses a disposable local page and a temporary Chrome profile, never the seat's signed-in profile. +Never starts a seat or activates the parent viewer; it renews the lease you already hold. +.DESCRIPTION +Checks that set_value on a text field still succeeds and reads back within its 1.5-second wait, that +set_value on a drop-down list (

+

+

+ + +"@ + +function Call-Anode([string[]]$Arguments) { + if ($Arguments[0] -in @('windows','inspect','window','element','wait') -and $Arguments -notcontains '--json') { $Arguments += '--json' } + $resultText = (& $Anode @Arguments | Out-String) + if ($LASTEXITCODE -ne 0) { throw "Anode command failed: $($Arguments[0])" } + return $resultText | ConvertFrom-Json +} +# For a command that may fail on purpose: its exit code and everything it wrote. +function Try-Anode([string[]]$Arguments) { + $savedPreference = $ErrorActionPreference + try { + $ErrorActionPreference = 'Continue' + $text = (& $Anode @Arguments 2>&1 | Out-String) + return [pscustomobject]@{ ExitCode = $LASTEXITCODE; Text = $text } + } finally { $ErrorActionPreference = $savedPreference } +} +function Assert-That($Condition, [string]$Message) { if (-not $Condition) { throw $Message } } +function Find-Control($Observation, [string]$AutomationId) { @($Observation.elements | Where-Object { $_.automationId -eq $AutomationId }) } + +$status = Call-Anode @('status', '--json') +Assert-That ($status.state -eq 'ready' -and $status.session -ne $status.parentSession) 'An existing verified seat is required.' +# The caller supplies an existing lease; never acquire/start a seat from this opt-in test. +$null = Call-Anode @('lease', 'renew', '--ttl', '600') +$url = ([Uri]$page).AbsoluteUri +$windowId = $null +# The Chrome processes this test started, by id and start time, so cleanup never touches another one. +$owned = @() +function Add-Owned([int]$ProcessId) { + $process = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue + if ($process -and $process.SessionId -eq $status.session -and -not ($script:owned | Where-Object { $_.Id -eq $ProcessId })) { + $script:owned += [pscustomobject]@{ Id = $ProcessId; Started = $process.StartTime } + } +} +$warnings = @() +try { + $launched = Call-Anode @('run', $Chrome, "--user-data-dir=$profileDirectory", '--no-first-run', '--no-default-browser-check', + '--force-renderer-accessibility', "--app=$url") + Assert-That ($launched.session -eq $status.session) 'Chrome launched in the wrong session.' + Add-Owned ([int]$launched.pid) + $deadline = (Get-Date).AddSeconds(20) + do { + $windows = @((Call-Anode @('windows', '--query', $title)).windows) + if ($windows.Count -eq 0) { Start-Sleep -Milliseconds 250 } + } while ($windows.Count -eq 0 -and (Get-Date) -lt $deadline) + Assert-That ($windows.Count -eq 1) "Chrome's window '$title' did not appear exactly once." + $windowId = $windows[0].windowId + $chromeProcess = Get-Process -Id ([int]$windows[0].pid) + Assert-That ($chromeProcess.SessionId -eq $status.session) 'Chrome window is outside the seat.' + Add-Owned $chromeProcess.Id + # Page controls sit deep in Chrome's tree, and Chrome builds the tree only once something asks. + function Observe-Page { Call-Anode @('inspect', $windowId, '--max-depth', '16', '--max-elements', '500') } + $deadline = (Get-Date).AddSeconds(15) + do { + $observation = Observe-Page + $ready = @(Find-Control $observation 'name').Count -gt 0 -and @(Find-Control $observation 'plan').Count -gt 0 + if (-not $ready) { Start-Sleep -Milliseconds 500 } + } while (-not $ready -and (Get-Date) -lt $deadline) + Assert-That $ready "The page's fields never appeared in Chrome's accessibility tree." + + # A text field: set_value succeeds only once the field reads the new value back. + $field = @(Find-Control $observation 'name')[0] + Assert-That ($field.actions -contains 'set_value') 'The text field offers no set_value.' + $watch = [Diagnostics.Stopwatch]::StartNew() + $null = Call-Anode @('element', $observation.snapshotId, $field.id, 'set_value', '--value', 'Sam Example') + $watch.Stop() + $fieldMilliseconds = $watch.ElapsedMilliseconds + $observation = Observe-Page + $field = @(Find-Control $observation 'name')[0] + Assert-That ($field.text -eq 'Sam Example') "The text field reads '$($field.text)' after set_value." + + # A drop-down list: Chrome may ignore set_value, which must then fail instead of reporting success. + $plan = @(Find-Control $observation 'plan')[0] + Assert-That ($plan.text -eq 'Free') "The drop-down list starts at '$($plan.text)', not Free." + $attempt = Try-Anode @('element', $observation.snapshotId, $plan.id, 'set_value', '--value', 'Team') + $observation = Observe-Page + $plan = @(Find-Control $observation 'plan')[0] + $result = @(Find-Control $observation 'result')[0] + if ($attempt.ExitCode -eq 0) { + Assert-That ($plan.text -eq 'Team' -and $result.text -eq 'Plan: Team') 'set_value on the drop-down list reported success, but the choice did not change.' + $dropDown = 'applied' + } else { + Assert-That ($attempt.Text -match 'still showed its old value') "set_value on the drop-down list failed for another reason: $($attempt.Text.Trim())" + Assert-That ($plan.text -eq 'Free' -and $result.text -eq 'Plan: Free') 'set_value on the drop-down list failed, but the choice changed.' + $dropDown = 'refused' + } + + # The open list: each control once, and the options if Chrome lists them. + Assert-That ($plan.actions -contains 'expand') 'The drop-down list offers no expand.' + $null = Call-Anode @('element', $observation.snapshotId, $plan.id, 'expand') + Start-Sleep -Milliseconds 500 + $open = Observe-Page + $copies = @(Find-Control $open 'name').Count + Assert-That ($copies -eq 1) "With the list open, the observation listed the text field $copies times." + $skippedRepeats = 0 + if ($open.PSObject.Properties['skippedRepeats']) { $skippedRepeats = [int]$open.skippedRepeats } + $target = if ($dropDown -eq 'applied') { 'Enterprise' } else { 'Team' } + $option = @($open.elements | Where-Object { $_.role -eq 'ListItem' -and $_.name -eq $target }) | Select-Object -First 1 + $optionChosen = $false + if (-not $option) { + $warnings += "The open list's options were not in the observation; choose an option with seat_screenshot and seat_click." + } else { + $action = @('select', 'invoke') | Where-Object { $option.actions -contains $_ } | Select-Object -First 1 + if (-not $action) { + $warnings += "The option '$target' was listed without select or invoke." + } else { + $null = Call-Anode @('element', $open.snapshotId, $option.id, $action) + $chosen = Try-Anode @('wait', $windowId, '--automation-id', 'result', '--text', "Plan: $target", '--wait', '5000', '--json') + $optionChosen = $chosen.ExitCode -eq 0 + if (-not $optionChosen) { $warnings += "'$action' on the option '$target' did not change the choice within 5 seconds." } + } + } + $finalStatus = Call-Anode @('status', '--json') + Assert-That ($finalStatus.session -eq $status.session -and $finalStatus.viewerVisible -eq $status.viewerVisible) 'Seat or parent viewer state changed.' + foreach ($warning in $warnings) { Write-Warning $warning } + [pscustomobject]@{ + passed = $true; session = $status.session; chrome = $Chrome; chromeVersion = (Get-Item -LiteralPath $Chrome).VersionInfo.ProductVersion + textFieldSetValueMs = $fieldMilliseconds; dropDownSetValue = $dropDown; openListSkippedRepeats = $skippedRepeats + optionsListed = [bool]$option; optionChosen = $optionChosen; warnings = $warnings + checks = @('page in the accessibility tree', 'text field set_value read back', 'drop-down set_value changes the choice or fails', + 'open list observed without repeats', 'parent viewer unchanged') + } | ConvertTo-Json -Depth 5 | Tee-Object -FilePath (Join-Path $OutputDirectory 'result.json') +} finally { + if ($windowId) { & $Anode window $windowId close | Out-Host } + foreach ($item in $owned) { + $remaining = Get-Process -Id $item.Id -ErrorAction SilentlyContinue + if ($remaining -and $remaining.SessionId -eq $status.session -and $remaining.StartTime -eq $item.Started) { + if (-not $remaining.WaitForExit(5000)) { & $Anode ps kill $item.Id | Out-Host } + } + } + # Chrome's helper processes can hold the profile for a moment after the browser exits. + for ($try = 0; $try -lt 10 -and (Test-Path -LiteralPath $profileDirectory); $try++) { + try { Remove-Item -LiteralPath $profileDirectory -Recurse -Force -ErrorAction Stop } catch { Start-Sleep -Milliseconds 500 } + } + if (Test-Path -LiteralPath $profileDirectory) { Write-Warning "The temporary Chrome profile is still at ${profileDirectory}; delete it once Chrome has exited." } +} diff --git a/server.json b/server.json index a0f8836..09859e9 100644 --- a/server.json +++ b/server.json @@ -3,7 +3,7 @@ "name": "io.github.skulitom/anode", "title": "Anode background Windows desktop", "description": "Background Windows desktop for AI agents: native GUI automation, screenshots and app testing.", - "version": "0.11.1", + "version": "0.11.2", "websiteUrl": "https://github.com/skulitom/Anode/blob/main/docs/INSTALL.md", "icons": [ { diff --git a/src/Anode/Anode.csproj b/src/Anode/Anode.csproj index 2372be4..c4de0e2 100644 --- a/src/Anode/Anode.csproj +++ b/src/Anode/Anode.csproj @@ -12,7 +12,7 @@ true anode Anode - 0.11.1 + 0.11.2 Anode Anode Anode background Windows desktop