diff --git a/.github/workflows/computer-use-native.yml b/.github/workflows/computer-use-native.yml index 36ad7d0da..10f048c60 100644 --- a/.github/workflows/computer-use-native.yml +++ b/.github/workflows/computer-use-native.yml @@ -59,6 +59,9 @@ jobs: if: runner.os == 'Windows' working-directory: target/computer-use/source.noindex/apps/OpenComputerUseWindows run: go test -p 2 ./... + - name: Measure the helper's PowerShell console with a visible control + if: runner.os == 'Windows' + run: python scripts/test-computer-use-windows-console.py target/computer-use/source.noindex target/computer-use/win32-x64/ocu.exe --report target/computer-use/windows-console.json - name: Test Linux native runtime regressions if: runner.os == 'Linux' working-directory: target/computer-use/source.noindex/apps/OpenComputerUseLinux @@ -87,7 +90,9 @@ jobs: if: runner.os == 'Windows' && always() with: name: computer-use-windows-fixture-evidence - path: target/computer-use/windows-fixture* + path: | + target/computer-use/windows-fixture* + target/computer-use/windows-console.json if-no-files-found: warn - uses: actions/upload-artifact@v4 if: always() diff --git a/.github/workflows/release-artifact-smoke.yml b/.github/workflows/release-artifact-smoke.yml index e56c25075..31358d77a 100644 --- a/.github/workflows/release-artifact-smoke.yml +++ b/.github/workflows/release-artifact-smoke.yml @@ -17,6 +17,10 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.26.8' + cache: false - name: Download Windows artifact from the release shell: pwsh env: @@ -83,6 +87,8 @@ jobs: if ($LASTEXITCODE -ne 0) { throw "Packaged native helper protocol check failed" } python scripts/test-computer-use-windows-fixture.py $helper --report artifact/windows-fixture.json if ($LASTEXITCODE -ne 0) { throw "Packaged native helper fixture check failed" } + python scripts/test-computer-use-packaged-windows-console.py --cli $cli --report artifact/windows-packaged-console.json + if ($LASTEXITCODE -ne 0) { throw "Packaged Rust-to-Go console check failed" } $cliVersion = & $cli --version if ($LASTEXITCODE -ne 0 -or $cliVersion -notmatch [regex]::Escape($env:VERSION)) { throw "Packaged CLI did not report $env:VERSION" @@ -113,6 +119,13 @@ jobs: Stop-Process -Id $process.Id -Force Write-Host "Windows packaged runtime smoke passed for v$env:VERSION" + - uses: actions/upload-artifact@v4 + if: always() + with: + name: release-windows-copilot-console + path: artifact/windows-packaged-console.json + if-no-files-found: warn + # ⚠ Proven able to fail, and repeatable: dispatch this workflow at a # version whose daemon HAS the defect and the step below must fail with # "PORT STILL HELD". Measured on v1.91.0 (2026-09-22, run 35763477213): diff --git a/.github/workflows/windows-gui-packages.yml b/.github/workflows/windows-gui-packages.yml index c73024476..b7058b268 100644 --- a/.github/workflows/windows-gui-packages.yml +++ b/.github/workflows/windows-gui-packages.yml @@ -120,6 +120,15 @@ jobs: - name: Build the Biorouter Copilot helper payload run: python scripts/computer-use-runtime.py build win32-x64 + - name: Measure the release helper's PowerShell console + run: python scripts/test-computer-use-windows-console.py target/computer-use/source.noindex target/computer-use/win32-x64/ocu.exe --report target/computer-use/windows-console.json + - uses: actions/upload-artifact@v4 + if: always() + with: + name: windows-package-console-evidence-${{ inputs.version }} + path: target/computer-use/windows-console.json + if-no-files-found: warn + # ⚠ `src/bin` is not tracked in git, so a fresh checkout has none. # `prepare-platform-binaries.js` VALIDATES this directory and never copies # from `target/`. Omitting this stage is how 1.91.0 produced an installer diff --git a/Cargo.lock b/Cargo.lock index 1a5c6dcf9..043d9ed34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -974,7 +974,7 @@ dependencies = [ [[package]] name = "biorouter" -version = "1.91.1" +version = "1.91.2" dependencies = [ "agent-client-protocol-schema", "ahash", @@ -1070,7 +1070,7 @@ dependencies = [ [[package]] name = "biorouter-acp" -version = "1.91.1" +version = "1.91.2" dependencies = [ "anyhow", "assert-json-diff", @@ -1096,7 +1096,7 @@ dependencies = [ [[package]] name = "biorouter-authprompt" -version = "1.91.1" +version = "1.91.2" dependencies = [ "block2", "objc2", @@ -1104,7 +1104,7 @@ dependencies = [ [[package]] name = "biorouter-bench" -version = "1.91.1" +version = "1.91.2" dependencies = [ "anyhow", "async-trait", @@ -1127,7 +1127,7 @@ dependencies = [ [[package]] name = "biorouter-cli" -version = "1.91.1" +version = "1.91.2" dependencies = [ "anstream", "anyhow", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "biorouter-mcp" -version = "1.91.1" +version = "1.91.2" dependencies = [ "aes-gcm", "ammonia", @@ -1285,7 +1285,7 @@ dependencies = [ [[package]] name = "biorouter-sandbox" -version = "1.91.1" +version = "1.91.2" dependencies = [ "anyhow", "async-trait", @@ -1302,7 +1302,7 @@ dependencies = [ [[package]] name = "biorouter-server" -version = "1.91.1" +version = "1.91.2" dependencies = [ "anyhow", "async-trait", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "biorouter-test" -version = "1.91.1" +version = "1.91.2" dependencies = [ "clap", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 34b2f417a..8b6b3cfbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "1.91.1" +version = "1.91.2" authors = ["Wanjun Gu "] license = "Apache-2.0" repository = "https://github.com/BaranziniLab/biorouter" diff --git a/README.md b/README.md index 0fe1117bd..3055e9c95 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

Apache 2.0 License - Version 1.91.1 + Version 1.91.2

Download · diff --git a/biorouter-self-test.yaml b/biorouter-self-test.yaml index 9fe7e8f12..ec6358a85 100644 --- a/biorouter-self-test.yaml +++ b/biorouter-self-test.yaml @@ -128,7 +128,7 @@ instructions: | - Test extension interactions - Verify isolation between extensions - In a disposable chat, browse `/` and `/ext:` and confirm current capability labels including Biorouter Copilot. Select a session-only extension and verify its exact reference identity. - - Submit `/extend computer controller` and verify actionable `/ext:` guidance without model execution or a permission grant. + - Submit `/extend Biorouter Copilot` and verify guidance names `/ext:BiorouterCopilot` without model execution or a permission grant. - Verify `/effort` and a harmless configured workflow in the desktop, TUI, and classic CLI. A missing workflow must report its problem instead of falling through to the model. - In a new desktop chat, verify `/diverge` is not suggested; typing it must preserve the draft and explain that an existing conversation is required. @@ -482,7 +482,12 @@ prompt: | Private and public chats must retain separate snapshots, trees, elements, results and grants. Verify a private-to-public handoff pauses before capture; do not transfer real private observations into the test report or a public model. - 6. Repeat through native direct calls, code execution and a coding-agent provider where + 6. On Windows, verify a packaged Copilot fixture call such as list_apps or get_app_state + does not show a console window for the Go helper's powershell.exe child. Pair the + measurement with a visible-console control and preserve the child-to-helper PID + relationship. A Node or Electron spawn probe does not exercise this path. If the + observer or packaged Windows runtime is unavailable, mark this NOT OBSERVABLE. + 7. Repeat through native direct calls, code execution and a coding-agent provider where available. Test Biorouter Copilot with Developer and Web & Documents disabled in an isolated profile. Missing GUI/provider evidence is NOT OBSERVABLE, never PASS. Record target host, helper version, exact fixture, tool results and remaining gaps in diff --git a/crates/biorouter-cli/src/session/completion.rs b/crates/biorouter-cli/src/session/completion.rs index 10ff8ecf7..7ef481478 100644 --- a/crates/biorouter-cli/src/session/completion.rs +++ b/crates/biorouter-cli/src/session/completion.rs @@ -192,6 +192,12 @@ pub(super) fn extension_reference_search_terms(name: &str) -> String { /// keep the compact marker when the real extractor proves it round-trips, and /// emit the canonical `` tag when it does not. pub(super) fn extension_marker(name: &str) -> String { + if name == "computercontroller" { + return reference_marker( + RefKind::Extension, + biorouter::agents::extension_manager::COPILOT_REFERENCE_ALIAS, + ); + } reference_marker(RefKind::Extension, name) } @@ -789,7 +795,7 @@ mod tests { ); } #[test] - fn completions_use_current_labels_and_preserve_canonical_markers() { + fn completions_use_current_labels_and_resolvable_markers() { let pairs = reference_pairs_from_names( "/copilot", Vec::::new(), @@ -797,7 +803,15 @@ mod tests { ); assert_eq!(pairs.len(), 1); assert_eq!(pairs[0].display, "/ext:Biorouter Copilot"); - assert_eq!(pairs[0].replacement, "/ext:computercontroller "); + assert_eq!(pairs[0].replacement, "/ext:BiorouterCopilot "); + let reference = extracted_refs(RefKind::Extension, &pairs[0].replacement); + assert_eq!(reference, ["BiorouterCopilot"]); + assert_eq!( + biorouter::agents::extension_manager::resolve_bundled_extension(&reference[0]) + .unwrap() + .key(), + "computercontroller" + ); assert!(SLASH_COMMANDS.contains(&"/effort")); let aliases = reference_pairs_from_names( "/ext:autovisualizer", diff --git a/crates/biorouter-cli/src/session/tui/mod.rs b/crates/biorouter-cli/src/session/tui/mod.rs index 708226b7c..249e3c573 100644 --- a/crates/biorouter-cli/src/session/tui/mod.rs +++ b/crates/biorouter-cli/src/session/tui/mod.rs @@ -2415,16 +2415,16 @@ mod tests { for command in biorouter::agents::execute_commands::list_commands() { assert!(backend_slash_command(&format!("/{} value", command.name))); } - assert!(backend_slash_command("/extend computer controller")); + assert!(backend_slash_command("/extend Biorouter Copilot")); assert!(backend_slash_command("/knowledge")); assert!(!backend_slash_command("/not-a-command")); } #[test] - fn computer_use_popup_shows_current_label_with_stable_wire_identity() { + fn copilot_popup_shows_current_label_with_stable_wire_identity() { let item = extension_completion_item("computercontroller", "Use this extension", true); assert_eq!(item.label, "Biorouter Copilot"); - assert_eq!(item.insert, "/ext:computercontroller "); + assert_eq!(item.insert, "/ext:BiorouterCopilot "); assert!(item.filter.contains("biorouter copilot")); let american = extension_completion_item("autovisualiser", "Use this extension", true); assert!(american.filter.contains("autovisualizer")); diff --git a/crates/biorouter-mcp/Cargo.toml b/crates/biorouter-mcp/Cargo.toml index 448d5abdf..96aa8a19c 100644 --- a/crates/biorouter-mcp/Cargo.toml +++ b/crates/biorouter-mcp/Cargo.toml @@ -127,4 +127,4 @@ ctor = "0.2" utoipa = ["dep:utoipa"] [target."cfg(windows)".dependencies] -windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_System_Diagnostics_ToolHelp", "Win32_System_JobObjects", "Win32_System_Threading"] } +windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_System_Diagnostics_ToolHelp", "Win32_System_JobObjects", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_UI_Shell"] } diff --git a/crates/biorouter-mcp/src/computer_use/manifest.rs b/crates/biorouter-mcp/src/computer_use/manifest.rs index 2755ab65e..421273026 100644 --- a/crates/biorouter-mcp/src/computer_use/manifest.rs +++ b/crates/biorouter-mcp/src/computer_use/manifest.rs @@ -2,10 +2,31 @@ use anyhow::{bail, Context, Result}; use serde::Deserialize; use sha2::{Digest, Sha256}; use std::path::{Component, Path, PathBuf}; +use std::sync::LazyLock; pub const UPSTREAM_VERSION: &str = "0.3.5"; pub const UPSTREAM_COMMIT: &str = "547b4ffb8ed731a8f16486e6d8a3b215484267d3"; +#[derive(Deserialize)] +struct RuntimePin { + schema_version: u32, + upstream_version: String, + upstream_commit: String, + patch_revision: String, +} + +static RUNTIME_PIN: LazyLock = LazyLock::new(|| { + serde_json::from_str(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../vendor/computer-use/pin.json" + ))) + .expect("bundled Biorouter Copilot pin must be valid") +}); + +pub fn patch_revision() -> &'static str { + &RUNTIME_PIN.patch_revision +} + #[derive(Debug, Deserialize)] pub struct Manifest { pub schema_version: u32, @@ -69,10 +90,10 @@ fn inspect(root: &Path, development_override: bool, verify_hashes: bool) -> Resu .context("computer_use_missing_runtime: manifest.json is unavailable")?; let manifest: Manifest = serde_json::from_slice(&bytes) .context("computer_use_incompatible_runtime: invalid manifest")?; - if manifest.schema_version != 1 - || manifest.upstream_version != UPSTREAM_VERSION - || manifest.upstream_commit != UPSTREAM_COMMIT - || manifest.patch_revision != "biorouter-1" + if manifest.schema_version != RUNTIME_PIN.schema_version + || manifest.upstream_version != RUNTIME_PIN.upstream_version + || manifest.upstream_commit != RUNTIME_PIN.upstream_commit + || manifest.patch_revision != RUNTIME_PIN.patch_revision || manifest.target != target() { bail!("computer_use_incompatible_runtime: runtime pin, patch or platform does not match BioRouter"); @@ -178,6 +199,40 @@ fn install_origin(bin: &Path) -> Option { .then(|| dunce::simplified(path).to_path_buf()) } +#[cfg(test)] +mod pin_tests { + use super::*; + + #[test] + fn current_bundled_pin_is_accepted_and_other_patch_revisions_are_refused() { + assert_eq!(RUNTIME_PIN.upstream_version, UPSTREAM_VERSION); + assert_eq!(RUNTIME_PIN.upstream_commit, UPSTREAM_COMMIT); + + let dir = tempfile::tempdir().unwrap(); + let executable = dir.path().join("ocu"); + std::fs::write(&executable, b"fixture helper").unwrap(); + let digest = format!("{:x}", Sha256::digest(std::fs::read(&executable).unwrap())); + let mut manifest = serde_json::json!({ + "schema_version": RUNTIME_PIN.schema_version, + "upstream_version": RUNTIME_PIN.upstream_version, + "upstream_commit": RUNTIME_PIN.upstream_commit, + "patch_revision": patch_revision(), + "target": target(), + "executable": "ocu", + "files": [{ "path": "ocu", "sha256": digest }], + }); + std::fs::write(dir.path().join("manifest.json"), manifest.to_string()).unwrap(); + inspect(dir.path(), false, true).expect("the bundled pin must be accepted"); + + manifest["patch_revision"] = serde_json::json!("outdated-patch"); + std::fs::write(dir.path().join("manifest.json"), manifest.to_string()).unwrap(); + assert!(inspect(dir.path(), false, true) + .unwrap_err() + .to_string() + .contains("computer_use_incompatible_runtime")); + } +} + #[cfg(all(test, windows))] mod windows_path_spelling_tests { use super::*; @@ -193,7 +248,7 @@ mod windows_path_spelling_tests { "schema_version": 1, "upstream_version": UPSTREAM_VERSION, "upstream_commit": UPSTREAM_COMMIT, - "patch_revision": "biorouter-1", + "patch_revision": patch_revision(), "target": target(), "executable": "ocu.exe", "files": [{ "path": "ocu.exe", "sha256": digest }], diff --git a/crates/biorouter-mcp/src/computer_use/runtime.rs b/crates/biorouter-mcp/src/computer_use/runtime.rs index df40ab3ac..3f6a97401 100644 --- a/crates/biorouter-mcp/src/computer_use/runtime.rs +++ b/crates/biorouter-mcp/src/computer_use/runtime.rs @@ -4,6 +4,8 @@ use rmcp::model::CallToolResult; use serde_json::{json, Value}; use sha2::{Digest, Sha256}; use std::process::Stdio; +#[cfg(windows)] +use std::{ffi::OsString, os::windows::ffi::OsStringExt, path::PathBuf}; use tokio::{ io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, process::{Child, ChildStdin, ChildStdout, Command}, @@ -34,7 +36,7 @@ impl Drop for Runtime { /// Pass through the desktop/session prerequisites the helper needs, and nothing /// else: model credentials and daemon secrets never reach it. -fn inherit_session_env(command: &mut Command) { +fn inherit_session_env(command: &mut Command) -> Result<()> { const SESSION: &[&str] = &[ "PATH", "HOME", @@ -64,13 +66,10 @@ fn inherit_session_env(command: &mut Command) { // own PATHEXT or ComSpec is strictly safer than clearing it: a cleared // variable falls back to a default the agent equally cannot influence. // - // Measured differential that motivated this: the SAME helper binary on the - // SAME runner image answers `doctor --json` in 2.17 s with the full - // inherited environment, and exceeds its 30 s timeout under this allowlist. - // - // PSModulePath is deliberately NOT here: it steers module autoloading, - // which is an execution-hijack surface, and Windows PowerShell recomputes a - // default when it is absent. + // On the same Windows runner, the pinned helper took 26.8 s to answer + // `doctor --json` without PSModulePath. System32 modules alone were also + // slow; System32 plus Program Files modules answered in 2.6 s. The inherited + // path was fast but may contain user-writable directories. #[cfg(windows)] const PLATFORM: &[&str] = &[ "ComSpec", @@ -101,6 +100,95 @@ fn inherit_session_env(command: &mut Command) { command.env(name, value); } } + #[cfg(windows)] + command.env("PSModulePath", trusted_powershell_module_path()?); + Ok(()) +} + +#[cfg(windows)] +fn trusted_powershell_module_path() -> Result { + use windows_sys::Win32::System::SystemInformation::GetSystemDirectoryW; + use windows_sys::Win32::UI::Shell::{SHGetFolderPathW, CSIDL_PROGRAM_FILES}; + + let mut system = [0u16; 260]; + let length = unsafe { GetSystemDirectoryW(system.as_mut_ptr(), system.len() as u32) } as usize; + if length == 0 || length >= system.len() { + bail!("computer_use_missing_dependency: Windows system directory unavailable"); + } + let system_modules = PathBuf::from(OsString::from_wide(&system[..length])) + .join("WindowsPowerShell") + .join("v1.0") + .join("Modules"); + + let mut program_files = [0u16; 260]; + let status = unsafe { + SHGetFolderPathW( + std::ptr::null_mut(), + CSIDL_PROGRAM_FILES as i32, + std::ptr::null_mut(), + 0, + program_files.as_mut_ptr(), + ) + }; + if status != 0 { + bail!("computer_use_missing_dependency: Windows Program Files directory unavailable"); + } + let length = program_files + .iter() + .position(|&unit| unit == 0) + .filter(|&length| length > 0) + .context("computer_use_missing_dependency: Windows Program Files directory invalid")?; + let all_users_modules = PathBuf::from(OsString::from_wide(&program_files[..length])) + .join("WindowsPowerShell") + .join("Modules"); + std::env::join_paths([system_modules, all_users_modules]) + .context("computer_use_missing_dependency: Windows PowerShell module path invalid") +} + +#[cfg(all(test, windows))] +mod windows_environment_tests { + use super::*; + + #[test] + fn helper_receives_only_machine_powershell_module_directories() { + let trusted = trusted_powershell_module_path().unwrap(); + let directories: Vec<_> = std::env::split_paths(&trusted).collect(); + assert_eq!(directories.len(), 2); + let system_modules = PathBuf::from(std::env::var_os("SystemRoot").unwrap()) + .join("System32") + .join("WindowsPowerShell") + .join("v1.0") + .join("Modules"); + let all_users_modules = PathBuf::from(std::env::var_os("ProgramFiles").unwrap()) + .join("WindowsPowerShell") + .join("Modules"); + assert!(directories[0] + .to_string_lossy() + .eq_ignore_ascii_case(&system_modules.to_string_lossy())); + assert!(directories[1] + .to_string_lossy() + .eq_ignore_ascii_case(&all_users_modules.to_string_lossy())); + let profile = PathBuf::from(std::env::var_os("USERPROFILE").unwrap()); + assert!(directories.iter().all(|path| !path.starts_with(&profile))); + + let mut command = Command::new("powershell.exe"); + command.env_clear(); + inherit_session_env(&mut command).unwrap(); + let env = command.as_std().get_envs().collect::>(); + let module_path = env + .iter() + .find(|(name, _)| name.to_string_lossy().eq_ignore_ascii_case("PSModulePath")) + .and_then(|(_, value)| *value) + .unwrap(); + assert_eq!(module_path, trusted.as_os_str()); + let path = env + .iter() + .find(|(name, _)| name.to_string_lossy().eq_ignore_ascii_case("PATH")) + .and_then(|(_, value)| *value) + .unwrap(); + let expected_path = std::env::var_os("PATH").unwrap(); + assert_eq!(path, expected_path.as_os_str()); + } } impl Runtime { @@ -123,7 +211,7 @@ impl Runtime { .args(arguments) .current_dir(&payload.root) .env_clear(); - inherit_session_env(&mut command); + inherit_session_env(&mut command)?; for name in ["TMPDIR", "TMP", "TEMP"] { command.env(name, temporary_files.path()); } diff --git a/crates/biorouter-mcp/tests/computer_use_contract.rs b/crates/biorouter-mcp/tests/computer_use_contract.rs index fdd79df2a..e37cf3e62 100644 --- a/crates/biorouter-mcp/tests/computer_use_contract.rs +++ b/crates/biorouter-mcp/tests/computer_use_contract.rs @@ -85,7 +85,7 @@ async fn tools_over_mcp( fn fixture(root: &Path) { std::fs::create_dir_all(root).unwrap(); std::fs::write(root.join("ocu"), b"fixture native bytes").unwrap(); - let manifest = json!({"schema_version":1,"upstream_version":manifest::UPSTREAM_VERSION,"upstream_commit":manifest::UPSTREAM_COMMIT,"patch_revision":"biorouter-1","target":manifest::target(),"executable":"ocu","files":[{"path":"ocu","sha256":format!("{:x}", Sha256::digest(b"fixture native bytes"))}]}); + let manifest = json!({"schema_version":1,"upstream_version":manifest::UPSTREAM_VERSION,"upstream_commit":manifest::UPSTREAM_COMMIT,"patch_revision":manifest::patch_revision(),"target":manifest::target(),"executable":"ocu","files":[{"path":"ocu","sha256":format!("{:x}", Sha256::digest(b"fixture native bytes"))}]}); std::fs::write( root.join("manifest.json"), serde_json::to_vec(&manifest).unwrap(), @@ -216,6 +216,7 @@ fn payload_rejects_corruption_wrong_pin_and_escaping_paths() { for (field, value) in [ ("upstream_version", "999"), ("upstream_commit", "unreviewed"), + ("patch_revision", "outdated"), ("target", "foreign"), ("executable", "../ocu"), ] { diff --git a/crates/biorouter-sandbox/src/console.rs b/crates/biorouter-sandbox/src/console.rs index 6fcd3fd06..23d70245c 100644 --- a/crates/biorouter-sandbox/src/console.rs +++ b/crates/biorouter-sandbox/src/console.rs @@ -8,8 +8,8 @@ //! therefore appear as the black window that flashes on screen and vanishes, //! once per tool call. //! -//! `CREATE_NO_WINDOW` is the documented answer: the child still gets a console -//! object (so its pipes behave normally), just no window for it. +//! `CREATE_NO_WINDOW` is the documented answer: the child's console handle is +//! not set, while redirected pipes continue to work. //! //! ⚠ **This cannot be fixed on the Electron side, so do not try.** `biorouterd.ts` //! already spawns the daemon with `windowsHide: true` *and* `detached: true` on @@ -19,9 +19,9 @@ //! console" would trade this bug for a worse one. The flag has to be set by //! whoever spawns the grandchild — which is this crate's callers. //! -//! A useful consequence: `CREATE_NO_WINDOW` yields a real-but-window-less -//! console that descendants **inherit**, so flagging an MCP server or a shell is -//! enough to keep the tools *it* runs quiet too. +//! The creation flag applies only to the process being started. A helper that +//! launches another console program must set the flag on that child too. The +//! Windows Copilot helper does this for its per-action PowerShell process. //! //! # Why this lives in the leaf crate //! diff --git a/crates/biorouter/src/agents/builtin_skills/about-biorouter/SKILL.md b/crates/biorouter/src/agents/builtin_skills/about-biorouter/SKILL.md index c9a72b33f..46e0be237 100644 --- a/crates/biorouter/src/agents/builtin_skills/about-biorouter/SKILL.md +++ b/crates/biorouter/src/agents/builtin_skills/about-biorouter/SKILL.md @@ -57,7 +57,7 @@ process), `streamable_http` (remote), or `inline_python`. Manage them from the ## Biorouter Copilot -Biorouter Copilot (`computercontroller`) is the sole built-in capability for observing or controlling +Biorouter Copilot is the sole built-in capability for observing or controlling applications and the desktop. Its native tools are `list_apps`, `get_app_state`, `click`, `perform_secondary_action`, `scroll`, `drag`, `type_text`, `press_key`, `set_value`, and `screen_capture`. Use the exact tools and argument schemas currently advertised. Developer does diff --git a/crates/biorouter/src/agents/builtin_skills/develop-biorouter/SKILL.md b/crates/biorouter/src/agents/builtin_skills/develop-biorouter/SKILL.md index bf381862a..cf018c90d 100644 --- a/crates/biorouter/src/agents/builtin_skills/develop-biorouter/SKILL.md +++ b/crates/biorouter/src/agents/builtin_skills/develop-biorouter/SKILL.md @@ -43,7 +43,7 @@ just install-deps # npm ci in ui/desktop (landing/video is a second, | `crates/biorouter` | Core agent library: agent loop, LLM providers, extension manager, session state, workflows, scheduler, privacy, context management | | `crates/biorouter-server` | Axum REST + WebSocket server, binary `biorouterd`; routes in `src/routes/` | | `crates/biorouter-cli` | Interactive CLI, binary `biorouter`; subcommands in `src/commands/` | -| `crates/biorouter-mcp` | Built-in MCP servers (developer, computer controller, memory, auto visualiser, knowledge, agent drafter, datasql, files, compute) | +| `crates/biorouter-mcp` | Built-in MCP servers (developer, Biorouter Copilot, memory, auto visualiser, knowledge, agent drafter, datasql, files, compute) | | `crates/biorouter-sandbox` | Capability-scoped sandboxed execution (docker, seatbelt, local) | | `crates/biorouter-acp` | Agent Communication Protocol for multi-agent orchestration | | `crates/biorouter-authprompt` | macOS auth prompt helper the background service cannot raise itself | diff --git a/crates/biorouter/src/agents/execute_commands.rs b/crates/biorouter/src/agents/execute_commands.rs index 79832fd65..ec1747dbd 100644 --- a/crates/biorouter/src/agents/execute_commands.rs +++ b/crates/biorouter/src/agents/execute_commands.rs @@ -49,8 +49,16 @@ pub fn list_commands() -> &'static [CommandDef] { fn extension_command_guidance(params: &str) -> String { match super::extension_manager::resolve_bundled_extension(params.trim()) { - Some(target) => format!("/extend is not a command. Select {} with /ext:{} followed by your request. Existing tool approvals still apply.", target.display_name(), target.key()), - None => "/extend is not a command. Use /ext: followed by your request, or choose an extension from the slash menu. For Biorouter Copilot, use /ext:computercontroller.".to_string(), + Some(target) => { + let key = target.key(); + let reference = if key == "computercontroller" { + super::extension_manager::COPILOT_REFERENCE_ALIAS + } else { + &key + }; + format!("/extend is not a command. Select {} with /ext:{reference} followed by your request. Existing tool approvals still apply.", target.display_name()) + } + None => format!("/extend is not a command. Use /ext: followed by your request, or choose an extension from the slash menu. For Biorouter Copilot, use /ext:{}.", super::extension_manager::COPILOT_REFERENCE_ALIAS), } } @@ -368,17 +376,19 @@ mod slash_command_audit_tests { #[test] fn legacy_extend_explains_the_current_name_and_marker() { - let guidance = extension_command_guidance("computer controller"); + let guidance = extension_command_guidance("Biorouter Copilot"); assert!(guidance.contains("Biorouter Copilot")); - assert!(guidance.contains("/ext:computercontroller")); + assert!(guidance.contains("/ext:BiorouterCopilot")); assert!(guidance.contains("approvals still apply")); - assert_eq!(guidance, extension_command_guidance("Biorouter Copilot")); + assert_eq!(guidance, extension_command_guidance("computer controller")); } #[test] fn unknown_extend_targets_receive_actionable_guidance() { let guidance = extension_command_guidance("unconfigured-extension"); assert!(guidance.contains("/ext:")); + assert!(guidance.contains("/ext:BiorouterCopilot")); + assert!(!guidance.contains("computercontroller")); assert!(guidance.contains("slash menu")); } @@ -398,9 +408,9 @@ mod slash_command_audit_tests { let config: SessionConfig = serde_json::from_value(serde_json::json!({"id": "slash-command-audit"})).unwrap(); for command in [ - "/extend computer controller", - "/extend\tcomputer controller", - "/extend\ncomputer controller", + "/extend Biorouter Copilot", + "/extend\tBiorouter Copilot", + "/extend\nBiorouter Copilot", "/knowledge", ] { assert!(agent diff --git a/crates/biorouter/src/agents/extension_manager.rs b/crates/biorouter/src/agents/extension_manager.rs index c9694346b..9f8e97d1c 100644 --- a/crates/biorouter/src/agents/extension_manager.rs +++ b/crates/biorouter/src/agents/extension_manager.rs @@ -733,6 +733,8 @@ fn extension_reference_key(name: &str) -> String { .collect() } +pub const COPILOT_REFERENCE_ALIAS: &str = "BiorouterCopilot"; + /// Resolve a `/ext:` target to the bundled extension it names, **by id and /// owning registry** rather than by display name. /// @@ -794,6 +796,13 @@ pub(crate) fn exact_custom_reference_key( target: Option<&BundledExtensionTarget>, active: &[ExtensionConfig], ) -> Option { + // The compact slash alias must retain the bundled target. A custom + // extension with the exact spaced display name keeps its existing routing. + if requested.eq_ignore_ascii_case(COPILOT_REFERENCE_ALIAS) + && target.is_some_and(|target| target.key() == "computercontroller") + { + return None; + } active .iter() .find(|config| { @@ -1231,7 +1240,7 @@ impl ExtensionManager { return Ok(None); } anyhow::ensure!(crate::security::computer_use::is_computer_use_tool(tool), - "This legacy Computer Controller tool was removed. Use the Biorouter Copilot tools or Web & Documents capability."); + "This former tool is unavailable. Use Biorouter Copilot's current tools or the Web & Documents capability."); let status = self.computer_use_status(session_id).await?; anyhow::ensure!( status.public_model != cap.tier().is_private(), @@ -1350,7 +1359,7 @@ impl ExtensionManager { && !matches!(&config, ExtensionConfig::Builtin { name, .. } if name == "computercontroller") { return Err(ExtensionError::ConfigError( - "The computercontroller name is reserved for the Biorouter Copilot built-in".into(), + "This extension name is reserved for the Biorouter Copilot built-in. Rename the custom extension.".into(), )); } @@ -5526,7 +5535,7 @@ mod tests { } #[test] - fn exact_active_custom_alias_is_not_redirected_to_a_builtin() { + fn copilot_slash_alias_is_reserved_without_redirecting_exact_custom_names() { let custom = |name: &str| ExtensionConfig::Frontend { name: name.to_string(), description: String::new(), @@ -5537,18 +5546,35 @@ mod tests { }; let target = resolve_bundled_extension("Biorouter Copilot").unwrap(); let entry = custom("Biorouter Copilot"); + let custom_key = entry.key(); assert_eq!( exact_custom_reference_key( "Biorouter Copilot", Some(&target), std::slice::from_ref(&entry) ), - Some(entry.key()) + Some(custom_key) ); assert_eq!( exact_custom_reference_key("computercontroller", Some(&target), &[entry]), None ); + assert_eq!( + exact_custom_reference_key( + COPILOT_REFERENCE_ALIAS, + Some(&target), + &[custom(COPILOT_REFERENCE_ALIAS)] + ), + None + ); + assert_eq!( + exact_custom_reference_key( + "bioroutercopilot", + Some(&target), + &[custom("bioroutercopilot")] + ), + None + ); let target = resolve_bundled_extension("developer").unwrap(); assert_eq!( exact_custom_reference_key("developer", Some(&target), &[custom("developer")]), @@ -5573,6 +5599,17 @@ mod tests { } } + #[test] + fn copilot_slash_alias_keeps_the_builtin_registry_identity() { + let target = resolve_bundled_extension(COPILOT_REFERENCE_ALIAS).unwrap(); + assert_eq!(target.key(), "computercontroller"); + assert_eq!(target.display_name(), "Biorouter Copilot"); + assert!(matches!( + target.into_config(String::new()), + ExtensionConfig::Builtin { name, .. } if name == "computercontroller" + )); + } + // ---- issue #48: `/ext:` resolution by id + owning registry ---- /// `/ext:extensionmanager` used to build an `ExtensionConfig::Builtin` named diff --git a/crates/biorouter/src/agents/snapshots/biorouter__agents__prompt_manager__tests__basic.snap b/crates/biorouter/src/agents/snapshots/biorouter__agents__prompt_manager__tests__basic.snap index ec5fb4b9a..5f6e82fb4 100644 --- a/crates/biorouter/src/agents/snapshots/biorouter__agents__prompt_manager__tests__basic.snap +++ b/crates/biorouter/src/agents/snapshots/biorouter__agents__prompt_manager__tests__basic.snap @@ -133,7 +133,7 @@ Prefer the simplest tool that does the job; reach for a specialized capability o ## Biorouter Copilot -Biorouter Copilot (`computercontroller`) is the sole built-in capability for observing or controlling +Biorouter Copilot is the sole built-in capability for observing or controlling applications and the desktop. Its native tools are `list_apps`, `get_app_state`, `click`, `perform_secondary_action`, `scroll`, `drag`, `type_text`, `press_key`, `set_value`, and `screen_capture`. Use the exact tools and argument schemas currently advertised. Developer does diff --git a/crates/biorouter/src/agents/snapshots/biorouter__agents__prompt_manager__tests__one_extension.snap b/crates/biorouter/src/agents/snapshots/biorouter__agents__prompt_manager__tests__one_extension.snap index 7899e714d..a3a369a9d 100644 --- a/crates/biorouter/src/agents/snapshots/biorouter__agents__prompt_manager__tests__one_extension.snap +++ b/crates/biorouter/src/agents/snapshots/biorouter__agents__prompt_manager__tests__one_extension.snap @@ -138,7 +138,7 @@ Prefer the simplest tool that does the job; reach for a specialized capability o ## Biorouter Copilot -Biorouter Copilot (`computercontroller`) is the sole built-in capability for observing or controlling +Biorouter Copilot is the sole built-in capability for observing or controlling applications and the desktop. Its native tools are `list_apps`, `get_app_state`, `click`, `perform_secondary_action`, `scroll`, `drag`, `type_text`, `press_key`, `set_value`, and `screen_capture`. Use the exact tools and argument schemas currently advertised. Developer does diff --git a/crates/biorouter/src/agents/snapshots/biorouter__agents__prompt_manager__tests__typical_setup.snap b/crates/biorouter/src/agents/snapshots/biorouter__agents__prompt_manager__tests__typical_setup.snap index 60b4d2c8b..903823c7e 100644 --- a/crates/biorouter/src/agents/snapshots/biorouter__agents__prompt_manager__tests__typical_setup.snap +++ b/crates/biorouter/src/agents/snapshots/biorouter__agents__prompt_manager__tests__typical_setup.snap @@ -143,7 +143,7 @@ Prefer the simplest tool that does the job; reach for a specialized capability o ## Biorouter Copilot -Biorouter Copilot (`computercontroller`) is the sole built-in capability for observing or controlling +Biorouter Copilot is the sole built-in capability for observing or controlling applications and the desktop. Its native tools are `list_apps`, `get_app_state`, `click`, `perform_secondary_action`, `scroll`, `drag`, `type_text`, `press_key`, `set_value`, and `screen_capture`. Use the exact tools and argument schemas currently advertised. Developer does diff --git a/crates/biorouter/src/config/extensions.rs b/crates/biorouter/src/config/extensions.rs index badb49a29..130107107 100644 --- a/crates/biorouter/src/config/extensions.rs +++ b/crates/biorouter/src/config/extensions.rs @@ -714,7 +714,7 @@ pub fn get_warnings() -> Vec { || matches!(bare, "automation_script" | "computer_control") }) { - warnings.push(format!("'{key}': legacy Computer Controller tool restrictions remain restricted. Web/document tools moved to Web & Documents; select the new native Biorouter Copilot tools explicitly to allow desktop control. Retired scripts are no longer callable.")); + warnings.push(format!("'{key}': previously saved tool restrictions remain in place. Web and document tools moved to Web & Documents; select Biorouter Copilot's native tools explicitly to allow desktop control. Retired scripts are no longer callable.")); } } if matches!(entry.config, ExtensionConfig::Sse { .. }) { diff --git a/crates/biorouter/src/prompts/system.md b/crates/biorouter/src/prompts/system.md index 8c2db7073..9a1f39af6 100644 --- a/crates/biorouter/src/prompts/system.md +++ b/crates/biorouter/src/prompts/system.md @@ -277,7 +277,7 @@ Prefer the simplest tool that does the job; reach for a specialized capability o ## Biorouter Copilot -Biorouter Copilot (`computercontroller`) is the sole built-in capability for observing or controlling +Biorouter Copilot is the sole built-in capability for observing or controlling applications and the desktop. Its native tools are `list_apps`, `get_app_state`, `click`, `perform_secondary_action`, `scroll`, `drag`, `type_text`, `press_key`, `set_value`, and `screen_capture`. Use the exact tools and argument schemas currently advertised. Developer does diff --git a/docs/releases/notes/v1.91.2.md b/docs/releases/notes/v1.91.2.md new file mode 100644 index 000000000..414a8d00a --- /dev/null +++ b/docs/releases/notes/v1.91.2.md @@ -0,0 +1,50 @@ +# Biorouter v1.91.2 + +This patch release brings the changes merged since v1.91.1 into the desktop, CLI, +and packaged apps. It improves chat navigation and reading, tightens Biorouter +Copilot's controls, and addresses Windows console flashes. + +## Biorouter Copilot + +- The status banner appears only in a conversation that has actually requested + Copilot. A completed banner can be dismissed and stays dismissed when you + switch conversations. A new request brings it back. During active control, + dismissing the details keeps Stop available. +- When macOS permissions are missing, setup names the exact helper executable + that needs access. This matters when installed and development copies have + separate Screen Recording grants. +- Prompts and slash-command guidance use Biorouter Copilot's current name. The + compact `/ext:BiorouterCopilot` command resolves to the existing capability; + stored settings and tool identities remain compatible. +- The app now validates the native helper against the same patch pin used to + build it, so the updated helper passes compatibility checks instead of being + rejected as an incompatible runtime. + +## Windows + +- Desktop and daemon child processes now suppress unwanted console windows. + The Copilot helper also hides the PowerShell process it starts for Windows + accessibility actions. Windows checks cover the child process directly. +- The packaged daemon is tested for releasing its listening port even when a + child it started remains alive. +- A concurrent delete in the memory store no longer makes a legitimate write + look like an attempted path escape. + +## Chat and interface + +- Selected text in answers and document previews can be quoted into the chat + composer. Drafts and quotes survive switching between existing chat tabs. +- The slash picker includes extensions active in the current conversation, + and terminal command routing follows the same catalog. Branch actions are + called Diverge. Reserved workflow names cannot intercept built-in commands. +- Markdown, tables, tool logs, and chat tabs are easier to read. Standard, + Large, and Larger font settings scale shared content and controls while + keeping the app's window layout stable. +- The README now leads with model choice, public and private separation, and + biomedical research, with the relevant privacy disclosures retained. + +## Release checks + +The release build uses the Windows and Linux Copilot helpers from their own +packaging jobs and builds only the Mac helpers locally. The Windows package +and native helper are checked on Windows before publication. diff --git a/scripts/check-computer-use-naming.sh b/scripts/check-computer-use-naming.sh index 314e5f38f..81811b45f 100755 --- a/scripts/check-computer-use-naming.sh +++ b/scripts/check-computer-use-naming.sh @@ -39,8 +39,7 @@ # * historical records and migration prose. docs/history/, docs/releases/notes/ # and the integration plan describe what the product USED to be called; a # release note is a record of what that version said, not a live product -# string. This check therefore polices CODE -- the strings a person reads out -# of the running app and CLI -- and leaves prose to review. +# string. This check polices code and agent-facing prompts, not historical prose. # * Rust test modules, `crates/*/tests/*.rs` and `*.test.ts(x)` files. One test # deliberately feeds "Computer Controller" as an impersonation attempt # (external_servers_cannot_impersonate_the_computer_use_approval_namespace); @@ -59,11 +58,12 @@ cd "$(dirname "$0")/.." status=0 -# Sources a person's words can reach. third_party/ is upstream, src/web/ is a -# built bundle, and *.test.* files carry deliberate adversarial input. +# Sources a person's or agent's words can reach. third_party/ is upstream, +# src/web/ is a built bundle, and *.test.* files carry adversarial input. scratch=$(mktemp -d) trap 'rm -rf "$scratch"' EXIT sources=$(git ls-files 'crates/*.rs' 'ui/desktop/src/*.ts' 'ui/desktop/src/*.tsx' \ + crates/biorouter/src/prompts crates/biorouter/src/agents/builtin_skills biorouter-self-test.yaml \ | grep -v '^third_party/' \ | grep -v '^ui/desktop/src/web/' \ | grep -vE '^crates/[^/]+/tests/' \ @@ -217,6 +217,17 @@ if [ -n "$offenders" ]; then status=1 fi +# Agent-facing prose must not teach the former name or expose the compatibility +# identifier. The self-test YAML keeps that identifier in config, so its prose +# is covered by the phrase checks above rather than this stricter rule. +prompt_sources=$(git ls-files crates/biorouter/src/prompts crates/biorouter/src/agents/builtin_skills) +offenders=$(printf '%s\n' "$prompt_sources" | xargs grep -niE 'computer[[:space:]-]*controller|open[[:space:]]+computer[[:space:]]+use' 2>/dev/null || true) +if [ -n "$offenders" ]; then + echo "Biorouter Copilot naming: agent-facing prompts must name the capability only as Biorouter Copilot:" >&2 + printf '%s\n' "$offenders" >&2 + status=1 +fi + if [ "$status" -eq 0 ]; then echo "Biorouter Copilot naming is consistent" fi diff --git a/scripts/computer-use-package-acceptance.py b/scripts/computer-use-package-acceptance.py index 5f7d37bba..2e25453ed 100644 --- a/scripts/computer-use-package-acceptance.py +++ b/scripts/computer-use-package-acceptance.py @@ -317,6 +317,8 @@ def verify(target): if platform == 'win32': run([PYTHON, 'scripts/test-computer-use-windows-fixture.py', helper, '--report', OUTPUT / 'windows-installed-fixture.json'], timeout=180) + run([PYTHON, 'scripts/test-computer-use-packaged-windows-console.py', + '--cli', cli, '--report', OUTPUT / 'windows-packaged-console.json'], timeout=180) local = directory / 'isolated-user' env = dict(os.environ, LOCALAPPDATA=str(local), BIOROUTER_DISABLE_KEYRING='true', BIOROUTER_PATH_ROOT=str(directory / 'isolated-config')) diff --git a/scripts/test-computer-use-packaged-windows-console.py b/scripts/test-computer-use-packaged-windows-console.py new file mode 100644 index 000000000..fab4227c8 --- /dev/null +++ b/scripts/test-computer-use-packaged-windows-console.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +"""Measure a packaged CLI's Rust-to-Go Copilot PowerShell launch on Windows.""" + +import argparse +import json +import os +from pathlib import Path +import queue +import shutil +import subprocess +import sys +import tempfile +import threading +import time +import traceback + + +CREATE_NO_WINDOW = 0x08000000 +PROBE_GO = r''' +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "unsafe" +) + +func consoleVerdict() string { + window, _, _ := syscall.NewLazyDLL("kernel32.dll").NewProc("GetConsoleWindow").Call() + if window == 0 { + return "none" + } + visible, _, _ := syscall.NewLazyDLL("user32.dll").NewProc("IsWindowVisible").Call(window) + if visible != 0 { + return "visible" + } + return "hidden" +} + +func startupInfo() (uint32, uint16) { + var info syscall.StartupInfo + info.Cb = uint32(unsafe.Sizeof(info)) + _ = syscall.GetStartupInfo(&info) + return info.Flags, info.ShowWindow +} + +func ancestry() (int, string, int, string) { + parentPID := os.Getppid() + snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) + if err != nil { + return parentPID, "", 0, "" + } + defer syscall.CloseHandle(snapshot) + var entry syscall.ProcessEntry32 + entry.Size = uint32(unsafe.Sizeof(entry)) + if err := syscall.Process32First(snapshot, &entry); err != nil { + return parentPID, "", 0, "" + } + processes := map[uint32]syscall.ProcessEntry32{} + for { + processes[entry.ProcessID] = entry + if err := syscall.Process32Next(snapshot, &entry); err != nil { + break + } + } + parent, found := processes[uint32(parentPID)] + if !found { + return parentPID, "", 0, "" + } + grandparent := processes[parent.ParentProcessID] + return parentPID, syscall.UTF16ToString(parent.ExeFile[:]), + int(parent.ParentProcessID), syscall.UTF16ToString(grandparent.ExeFile[:]) +} + +func main() { + executable, err := os.Executable() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + directory := filepath.Dir(executable) + parentPID, parentImage, grandparentPID, grandparentImage := ancestry() + startupFlags, showWindow := startupInfo() + control := len(os.Args) == 2 && os.Args[1] == "--probe-only" + record, err := json.Marshal(map[string]any{ + "pid": os.Getpid(), "parent_pid": parentPID, + "parent_image": parentImage, "grandparent_pid": grandparentPID, + "grandparent_image": grandparentImage, + "mode": map[bool]string{true: "control", false: "forward"}[control], + "verdict": consoleVerdict(), + "startup_flags": startupFlags, "show_window": showWindow, + "startup_hidden": startupFlags&syscall.STARTF_USESHOWWINDOW != 0 && showWindow == syscall.SW_HIDE, + }) + if err == nil { + err = os.WriteFile(filepath.Join(directory, fmt.Sprintf("console-%d.json", os.Getpid())), record, 0600) + } + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + if control { + return + } + real, err := os.ReadFile(filepath.Join(directory, "real-powershell.txt")) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + child := exec.Command(strings.TrimSpace(string(real)), os.Args[1:]...) + child.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000, HideWindow: true} + child.Stdout, child.Stderr = os.Stdout, os.Stderr + if err := child.Run(); err != nil { + if exit, ok := err.(*exec.ExitError); ok { + os.Exit(exit.ExitCode()) + } + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } +} +''' + + +def records(directory, mode): + found = [json.loads(path.read_text(encoding="utf-8")) + for path in directory.glob("console-*.json")] + return [item for item in found if item.get("mode") == mode] + + +def observe(cli, wrapper): + directory = wrapper.parent + control = subprocess.run([str(wrapper), "--probe-only"], cwd=directory, + stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + timeout=15, check=False) + positive = records(directory, "control") + if (control.returncode or len(positive) != 1 or positive[0].get("verdict") != "visible" + or positive[0].get("startup_hidden") is not False): + raise AssertionError(f"GUI positive control did not show an unhidden console: {positive}, exit={control.returncode}") + + env = dict(os.environ) + env.pop("BIOROUTER_COMPUTER_USE_DIR", None) + env["PATH"] = str(directory) + os.pathsep + env.get("PATH", "") + messages = [ + {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { + "protocolVersion": "2025-03-26", "capabilities": {}, + "clientInfo": {"name": "Biorouter-package-console-check", "version": "1"}}}, + {"jsonrpc": "2.0", "method": "notifications/initialized"}, + {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { + "_meta": {"biorouter-session-id": "packaged-console-fixture", + "computer_use_generation": "packaged-console-fixture"}, + "name": "list_apps", "arguments": {}}}, + ] + errors_path = directory / "packaged-cli.stderr" + errors_file = errors_path.open("w", encoding="utf-8") + process = subprocess.Popen([str(cli), "mcp", "computercontroller"], cwd=directory, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=errors_file, + text=True, encoding="utf-8", errors="replace", + creationflags=CREATE_NO_WINDOW, env=env) + lines = queue.Queue() + def read_lines(): + for line in process.stdout: + lines.put(line) + lines.put(None) + threading.Thread(target=read_lines, daemon=True).start() + replies = {} + try: + for message in messages: + process.stdin.write(json.dumps(message) + "\n") + process.stdin.flush() + if "id" not in message: + continue + deadline = time.monotonic() + 90 + while message["id"] not in replies: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise AssertionError(f"packaged MCP request {message['id']} timed out") + try: + line = lines.get(timeout=remaining) + except queue.Empty as error: + raise AssertionError(f"packaged MCP request {message['id']} timed out") from error + if line is None: + raise AssertionError(f"packaged MCP exited before response {message['id']}") + reply = json.loads(line) + if "id" in reply: + replies[reply["id"]] = reply + process.stdin.close() + process.wait(timeout=15) + finally: + if process.poll() is None: + process.kill() + process.wait(timeout=5) + errors_file.close() + errors = errors_path.read_text(encoding="utf-8", errors="replace") + if process.returncode or not replies.get(1, {}).get("result", {}).get("serverInfo"): + raise AssertionError(f"packaged MCP initialization failed: exit={process.returncode}, stderr={errors[:1000]!r}") + result = replies.get(2, {}).get("result", {}) + if replies.get(2, {}).get("error") or result.get("isError") or not result.get("content"): + raise AssertionError(f"packaged list_apps failed: reply={replies.get(2)}, stderr={errors[:1000]!r}") + + children = records(directory, "forward") + if not children: + raise AssertionError("packaged Rust-to-Go call launched no instrumented PowerShell child") + for child in children: + if (child.get("parent_image", "").lower() != "ocu.exe" + or child.get("grandparent_image", "").lower() != "biorouter.exe" + or child.get("grandparent_pid") != process.pid): + raise AssertionError(f"PowerShell child did not descend from the packaged CLI and helper: {child}") + if child.get("verdict") == "visible": + raise AssertionError(f"packaged Copilot PowerShell child showed a console: {child}") + if child.get("verdict") not in {"none", "hidden"}: + raise AssertionError(f"PowerShell child did not report a console state: {child}") + if child.get("startup_hidden") is not True: + raise AssertionError(f"packaged Go helper did not request a hidden PowerShell startup: {child}") + return {"positive_control": positive[0], "packaged_cli_pid": process.pid, + "native_power_shell_children": children, "list_apps_success": True, + "coverage": "packaged CLI -> Rust MCP -> packaged Go helper -> PowerShell child"} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cli", required=True, type=Path) + parser.add_argument("--report", required=True, type=Path) + parser.add_argument("--worker", action="store_true") + parser.add_argument("--wrapper", type=Path) + args = parser.parse_args() + if os.name != "nt": + parser.error("the packaged console probe requires Windows") + report = args.report.resolve() + report.parent.mkdir(parents=True, exist_ok=True) + if args.worker: + try: + result = observe(args.cli.resolve(), args.wrapper.resolve()) + except Exception as error: + result = {"error": f"{type(error).__name__}: {error}", "traceback": traceback.format_exc()} + report.write_text(json.dumps(result, indent=2), encoding="utf-8") + return 1 if "error" in result else 0 + + try: + pythonw = Path(sys.executable).with_name("pythonw.exe") + if not pythonw.is_file(): + raise FileNotFoundError(f"GUI-subsystem Python is unavailable: {pythonw}") + real_powershell = shutil.which("powershell.exe") + if not real_powershell: + raise FileNotFoundError("Windows PowerShell is unavailable") + with tempfile.TemporaryDirectory(prefix="biorouter-packaged-console-") as temp: + directory = Path(temp) + wrapper = directory / "powershell.exe" + source = directory / "console_probe.go" + source.write_text(PROBE_GO, encoding="utf-8") + (directory / "real-powershell.txt").write_text(real_powershell, encoding="utf-8") + subprocess.run(["go", "build", "-trimpath", "-buildvcs=false", "-o", str(wrapper), str(source)], + check=True, timeout=90) + completed = subprocess.run( + [str(pythonw), str(Path(__file__).resolve()), "--worker", "--cli", str(args.cli.resolve()), + "--wrapper", str(wrapper), "--report", str(report)], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=120, check=False) + if not report.is_file(): + raise AssertionError(f"GUI observer wrote no report (exit {completed.returncode})") + result = json.loads(report.read_text(encoding="utf-8")) + if completed.returncode or "error" in result: + raise AssertionError(f"packaged Windows console check failed: {result}") + print(json.dumps(result, sort_keys=True)) + except Exception as error: + if not report.is_file(): + report.write_text(json.dumps({"error": f"{type(error).__name__}: {error}", + "traceback": traceback.format_exc()}, indent=2), encoding="utf-8") + raise + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/test-computer-use-protocol.py b/scripts/test-computer-use-protocol.py index b4b98905f..f38f3c1f1 100644 --- a/scripts/test-computer-use-protocol.py +++ b/scripts/test-computer-use-protocol.py @@ -4,11 +4,13 @@ import json import os from pathlib import Path +import re import subprocess TOOLS = {"list_apps", "get_app_state", "click", "perform_secondary_action", "scroll", "drag", "type_text", "press_key", "set_value", "screen_capture"} CONTRACT = Path(__file__).resolve().parents[1] / "crates/biorouter-mcp/tests/fixtures/computer-use-tools.json" +RETIRED_PROMPT_NAME = re.compile(r"(?:open[ _-]*computer[ _-]*use|computer[ _-]*controller|computer[ _-]*use)(?![a-z])", re.I) def semantic_schema(value): @@ -20,6 +22,18 @@ def semantic_schema(value): return value +def schema_descriptions(value): + if isinstance(value, dict): + for key, item in value.items(): + if key == "description" and isinstance(item, str): + yield item + else: + yield from schema_descriptions(item) + elif isinstance(value, list): + for item in value: + yield from schema_descriptions(item) + + def validate_tools(tools): expected = json.loads(CONTRACT.read_text())["tools"] names = {tool["name"] for tool in tools} @@ -27,6 +41,9 @@ def validate_tools(tools): raise ValueError(f"Native tool contract mismatch: received {sorted(names)}") for tool in expected: actual = next(item for item in tools if item["name"] == tool["name"]) + prompt_text = [actual.get("description", ""), *schema_descriptions(actual.get("inputSchema"))] + if any(RETIRED_PROMPT_NAME.search(text) for text in prompt_text): + raise ValueError(f"Retired Copilot name in native tool description: {tool['name']}") actual_schema = json.dumps(semantic_schema(actual.get("inputSchema")), sort_keys=True) expected_schema = json.dumps(tool["inputSchema"], sort_keys=True) if actual_schema != expected_schema: @@ -50,6 +67,9 @@ def check(directory): if not replies.get(1, {}).get("result", {}).get("serverInfo"): raise ValueError("Native helper failed MCP initialization") initialized = replies[1]["result"] + instructions = initialized.get("instructions", "") + if "Biorouter Copilot" not in instructions or RETIRED_PROMPT_NAME.search(instructions): + raise ValueError("Native helper instructions must name Biorouter Copilot") if initialized.get("protocolVersion") != "2025-03-26": raise ValueError("Native helper MCP protocol version drift") if initialized["serverInfo"].get("version") != manifest["upstream_version"]: diff --git a/scripts/test-computer-use-windows-console.py b/scripts/test-computer-use-windows-console.py new file mode 100644 index 000000000..45aa353f5 --- /dev/null +++ b/scripts/test-computer-use-windows-console.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +"""Compare Windows Copilot helper child-console behavior with its pre-fix build.""" + +import argparse +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import traceback + + +ROOT = Path(__file__).resolve().parents[1] +PATCH = ROOT / "vendor/computer-use/patches/0003-windows-console.patch" +CREATE_NO_WINDOW = 0x08000000 + +# A GUI observer cannot reliably attach to another process's console on CI. +# This child reports its own console handle before forwarding to PowerShell. +PROBE_GO = r''' +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "syscall" + "unsafe" +) + +func consoleVerdict() string { + kernel := syscall.NewLazyDLL("kernel32.dll") + user := syscall.NewLazyDLL("user32.dll") + window, _, _ := kernel.NewProc("GetConsoleWindow").Call() + if window == 0 { + return "none" + } + visible, _, _ := user.NewProc("IsWindowVisible").Call(window) + if visible != 0 { + return "visible" + } + return "hidden" +} + +func startupInfo() (uint32, uint16) { + var info syscall.StartupInfo + info.Cb = uint32(unsafe.Sizeof(info)) + _ = syscall.GetStartupInfo(&info) + return info.Flags, info.ShowWindow +} + +func main() { + report := os.Getenv("BIOROUTER_CONSOLE_PROBE_FILE") + if report == "" { + fmt.Fprintln(os.Stderr, "missing console probe report path") + os.Exit(2) + } + startupFlags, showWindow := startupInfo() + result, err := json.Marshal(map[string]any{ + "verdict": consoleVerdict(), + "pid": os.Getpid(), + "startup_flags": startupFlags, + "show_window": showWindow, + "startup_hidden": startupFlags&syscall.STARTF_USESHOWWINDOW != 0 && showWindow == syscall.SW_HIDE, + }) + if err == nil { + err = os.WriteFile(report, append(result, '\n'), 0600) + } + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + if os.Getenv("BIOROUTER_CONSOLE_PROBE_CONTROL") == "1" { + return + } + + real := os.Getenv("BIOROUTER_REAL_POWERSHELL") + if real == "" { + fmt.Fprintln(os.Stderr, "missing real PowerShell path") + os.Exit(2) + } + child := exec.Command(real, os.Args[1:]...) + child.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: 0x08000000, + HideWindow: true, + } + child.Stdout = os.Stdout + child.Stderr = os.Stderr + if err := child.Run(); err != nil { + if exit, ok := err.(*exec.ExitError); ok { + os.Exit(exit.ExitCode()) + } + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } +} +''' + + +def build_baseline(source, destination): + with tempfile.TemporaryDirectory(prefix="biorouter-pre-fix-helper-") as temp: + root = Path(temp) + app = root / "apps/OpenComputerUseWindows" + shutil.copytree(source / "apps/OpenComputerUseWindows", app) + subprocess.run(["git", "init", "--quiet"], cwd=root, check=True) + subprocess.run(["git", "apply", "--reverse", "--check", str(PATCH)], cwd=root, check=True) + subprocess.run(["git", "apply", "--reverse", str(PATCH)], cwd=root, check=True) + subprocess.run( + ["go", "build", "-trimpath", "-buildvcs=false", "-o", str(destination), "."], + cwd=app, + check=True, + ) + + +def build_probe(destination): + source = destination.with_suffix(".go") + source.write_text(PROBE_GO, encoding="utf-8") + subprocess.run( + ["go", "build", "-trimpath", "-buildvcs=false", "-o", str(destination), str(source)], + check=True, + ) + + +def probe_environment(wrapper, report, real_powershell, control=False): + env = dict(os.environ) + env["PATH"] = str(wrapper.parent) + os.pathsep + env.get("PATH", "") + env["BIOROUTER_CONSOLE_PROBE_FILE"] = str(report) + env["BIOROUTER_REAL_POWERSHELL"] = str(real_powershell) + env["OPEN_COMPUTER_USE_DISABLE_APP_AGENT_PROXY"] = "1" + if control: + env["BIOROUTER_CONSOLE_PROBE_CONTROL"] = "1" + else: + env.pop("BIOROUTER_CONSOLE_PROBE_CONTROL", None) + return env + + +def read_probe(path, label): + if not path.is_file(): + raise AssertionError(f"{label}: PowerShell child did not write a console verdict") + result = json.loads(path.read_text(encoding="utf-8")) + if result.get("verdict") not in {"visible", "hidden", "none"} or not result.get("pid"): + raise AssertionError(f"{label}: invalid console verdict: {result}") + return result + + +def run_control(wrapper, work, real_powershell): + report = work / "control-console.json" + completed = subprocess.run( + [str(wrapper)], + cwd=work, + env=probe_environment(wrapper, report, real_powershell, control=True), + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + timeout=15, + check=False, + ) + if completed.returncode: + raise AssertionError(f"unflagged control failed: {completed.stderr[:1000]!r}") + result = read_probe(report, "unflagged control") + if result["verdict"] != "visible" or result.get("startup_hidden") is not False: + raise AssertionError(f"unflagged control did not show an unhidden console: {result}") + return result + + +def run_helper(helper, wrapper, real_powershell, work, label): + report = work / f"{label}-console.json" + output = work / f"{label}.stdout" + errors = work / f"{label}.stderr" + with output.open("wb") as stdout, errors.open("wb") as stderr: + process = subprocess.Popen( + [str(helper), "call", "list_apps"], + cwd=work, + stdout=stdout, + stderr=stderr, + creationflags=CREATE_NO_WINDOW, + env=probe_environment(wrapper, report, real_powershell), + ) + try: + code = process.wait(timeout=45) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + raise AssertionError(f"{label}: helper timed out") + verdict = read_probe(report, label) + if code: + raise AssertionError( + f"{label}: list_apps failed with exit {code}: " + f"{errors.read_text(errors='replace')[:1000]}" + ) + result = json.loads(output.read_text(encoding="utf-8-sig")) + if not isinstance(result, dict) or result.get("isError") or not result.get("content"): + raise AssertionError(f"{label}: list_apps did not return a successful tool result") + return {"helper_pid": process.pid, **verdict} + + +def observe(patched, baseline, wrapper, real_powershell): + with tempfile.TemporaryDirectory(prefix="biorouter-console-observer-") as temp: + work = Path(temp) + positive = run_control(wrapper, work, real_powershell) + before = run_helper(baseline, wrapper, real_powershell, work, "before") + after = run_helper(patched, wrapper, real_powershell, work, "after") + if after["verdict"] == "visible": + raise AssertionError(f"patched helper showed its PowerShell child: {after}") + if before.get("startup_hidden") is not False: + raise AssertionError(f"pre-fix helper unexpectedly requested a hidden PowerShell startup: {before}") + if after.get("startup_hidden") is not True: + raise AssertionError(f"patched helper did not request a hidden PowerShell startup: {after}") + return { + "positive_control": positive, + "pre_fix_helper": before, + "pre_fix_reproduced": before["verdict"] == "visible", + "patched_helper": after, + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("source", type=Path, nargs="?") + parser.add_argument("patched", type=Path, nargs="?") + parser.add_argument("--report", type=Path, required=True) + parser.add_argument("--worker", action="store_true") + parser.add_argument("--baseline", type=Path) + parser.add_argument("--wrapper", type=Path) + parser.add_argument("--real-powershell", type=Path) + args = parser.parse_args() + if os.name != "nt": + parser.error("the console probe requires Windows") + args.report.parent.mkdir(parents=True, exist_ok=True) + + if args.worker: + try: + result = observe(args.patched, args.baseline, args.wrapper, args.real_powershell) + except Exception as error: + result = { + "error": f"{type(error).__name__}: {error}", + "traceback": traceback.format_exc(), + } + args.report.write_text(json.dumps(result, indent=2), encoding="utf-8") + return 1 if "error" in result else 0 + + pythonw = Path(sys.executable).with_name("pythonw.exe") + if not pythonw.is_file(): + raise FileNotFoundError(f"GUI-subsystem Python is unavailable: {pythonw}") + real_powershell = shutil.which("powershell.exe") + if not real_powershell: + raise FileNotFoundError("Windows PowerShell is unavailable") + with tempfile.TemporaryDirectory(prefix="biorouter-console-baseline-") as temp: + root = Path(temp) + baseline = root / "ocu-before-console-fix.exe" + wrapper = root / "powershell.exe" + build_baseline(args.source.resolve(), baseline) + build_probe(wrapper) + completed = subprocess.run( + [ + str(pythonw), __file__, "--worker", "--baseline", str(baseline), + "--wrapper", str(wrapper), "--real-powershell", real_powershell, + "--report", str(args.report), str(args.source), str(args.patched.resolve()), + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=120, + check=False, + ) + if not args.report.is_file(): + raise AssertionError(f"GUI observer wrote no report (exit {completed.returncode})") + result = json.loads(args.report.read_text(encoding="utf-8")) + if completed.returncode or "error" in result: + raise AssertionError(f"Windows helper console probe failed: {result}") + print(json.dumps(result, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 50f7d97d8..45f22544a 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "Apache-2.0" }, - "version": "1.91.1" + "version": "1.91.2" }, "paths": { "/action-required/secrets": { diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index 05041e732..f30b2a1e6 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "biorouter-app", - "version": "1.91.1", + "version": "1.91.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "biorouter-app", - "version": "1.91.1", + "version": "1.91.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 3a38452cf..42f8ef832 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -1,7 +1,7 @@ { "name": "biorouter-app", "productName": "Biorouter", - "version": "1.91.1", + "version": "1.91.2", "description": "Biorouter App", "engines": { "node": "^24.0.0" diff --git a/ui/desktop/scripts/console-window-census.mjs b/ui/desktop/scripts/console-window-census.mjs index 7701e7561..17cd24c6d 100644 --- a/ui/desktop/scripts/console-window-census.mjs +++ b/ui/desktop/scripts/console-window-census.mjs @@ -456,9 +456,8 @@ export function auditTree(root = SRC_ROOT) { for (const site of collectSpawnSites(text, repoRelative)) { sites.push({ ...site, file: repoRelative }); - // Rule 2, checked first because it is the one that decides what the user - // sees. An inherited fd stops libuv applying CREATE_NO_WINDOW, so this - // site shows a console window no matter what `windowsHide` says. + // Rule 2: inherited stdio prevents CREATE_NO_WINDOW. SW_HIDE can still + // hide the window, but this census requires the stronger no-console path. if (site.stdio === 'inherit' || site.stdio === 'fork-default' || site.stdio === 'unknown') { violations.push({ ...site, @@ -467,8 +466,8 @@ export function auditTree(root = SRC_ROOT) { site.stdio === 'unknown' ? 'stdio is not a literal this census can read, so whether it inherits an fd is unknown' : site.stdio === 'fork-default' - ? "fork() without `silent: true` defaults to stdio 'inherit', which shows a console window" - : 'stdio inherits a standard handle, so CREATE_NO_WINDOW is never applied and this shows a console window whatever windowsHide says', + ? "fork() without `silent: true` defaults to stdio 'inherit', preventing CREATE_NO_WINDOW" + : 'stdio inherits a standard handle, so CREATE_NO_WINDOW is never applied', }); continue; } diff --git a/ui/desktop/scripts/windows-console.test.mjs b/ui/desktop/scripts/windows-console.test.mjs index ceb166cdc..bc92c1a21 100644 --- a/ui/desktop/scripts/windows-console.test.mjs +++ b/ui/desktop/scripts/windows-console.test.mjs @@ -36,10 +36,9 @@ * `windowsHide` must show a VISIBLE console. If that control ever reports * hidden, this machine cannot show the window at all and every other * assertion in the file is vacuous — so it is an assertion, never a skip. - * * The grandchild case settles a site that looks unguarded and is not: the - * Biorouter Copilot helper runs `powershell.exe` per desktop action with no - * creation flags, but the daemon starts that helper with CREATE_NO_WINDOW and - * a child inherits its parent's windowless console. + * * The `cmd.exe` grandchild case measures only that specific shell path. The + * Biorouter Copilot Go helper starts PowerShell separately and must give + * that child its own creation flags. * * The Electron block pins the embedder behaviour. If a future Electron stops * setting kHideConsoleWindows, `pipe-without-windowsHide` flips to visible * and this goes red — which is the early warning the app currently does not diff --git a/vendor/computer-use/patches/0003-windows-console.patch b/vendor/computer-use/patches/0003-windows-console.patch new file mode 100644 index 000000000..7b6b3635b --- /dev/null +++ b/vendor/computer-use/patches/0003-windows-console.patch @@ -0,0 +1,76 @@ +diff --git a/apps/OpenComputerUseWindows/console_other.go b/apps/OpenComputerUseWindows/console_other.go +new file mode 100644 +index 0000000..922ce8e +--- /dev/null ++++ b/apps/OpenComputerUseWindows/console_other.go +@@ -0,0 +1,7 @@ ++//go:build !windows ++ ++package main ++ ++import "os/exec" ++ ++func configurePowerShellChild(_ *exec.Cmd) {} +diff --git a/apps/OpenComputerUseWindows/console_windows.go b/apps/OpenComputerUseWindows/console_windows.go +new file mode 100644 +index 0000000..33aaeb2 +--- /dev/null ++++ b/apps/OpenComputerUseWindows/console_windows.go +@@ -0,0 +1,18 @@ ++//go:build windows ++ ++package main ++ ++import ( ++ "os/exec" ++ "syscall" ++) ++ ++const createNoWindow = 0x08000000 ++ ++// The Rust launcher hides the Go helper, but its children need their own flags. ++func configurePowerShellChild(cmd *exec.Cmd) { ++ cmd.SysProcAttr = &syscall.SysProcAttr{ ++ CreationFlags: createNoWindow, ++ HideWindow: true, ++ } ++} +diff --git a/apps/OpenComputerUseWindows/console_windows_test.go b/apps/OpenComputerUseWindows/console_windows_test.go +new file mode 100644 +index 0000000..db74ab3 +--- /dev/null ++++ b/apps/OpenComputerUseWindows/console_windows_test.go +@@ -0,0 +1,21 @@ ++//go:build windows ++ ++package main ++ ++import ( ++ "os/exec" ++ "strings" ++ "testing" ++) ++ ++func TestPowerShellChildSetsNoWindowFlagsAndKeepsOutput(t *testing.T) { ++ cmd := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-Command", "Write-Output ready") ++ configurePowerShellChild(cmd) ++ if cmd.SysProcAttr == nil || cmd.SysProcAttr.CreationFlags&createNoWindow == 0 || !cmd.SysProcAttr.HideWindow { ++ t.Fatalf("PowerShell child must use CREATE_NO_WINDOW and SW_HIDE: %#v", cmd.SysProcAttr) ++ } ++ output, err := cmd.Output() ++ if err != nil || strings.TrimSpace(string(output)) != "ready" { ++ t.Fatalf("PowerShell output must still reach the helper: output=%q, error=%v", output, err) ++ } ++} +diff --git a/apps/OpenComputerUseWindows/main.go b/apps/OpenComputerUseWindows/main.go +index 691435d..efba13e 100644 +--- a/apps/OpenComputerUseWindows/main.go ++++ b/apps/OpenComputerUseWindows/main.go +@@ -585,6 +585,7 @@ func runPowerShell(request psRequest) (*psResponse, error) { + defer cancel() + + cmd := exec.CommandContext(ctx, "powershell.exe", "-MTA", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath, operationPath) ++ configurePowerShellChild(cmd) + output, err := cmd.CombinedOutput() + if ctx.Err() == context.DeadlineExceeded { + return nil, errors.New("Windows runtime timed out after 30s") diff --git a/vendor/computer-use/patches/0004-copilot-prompts.patch b/vendor/computer-use/patches/0004-copilot-prompts.patch new file mode 100644 index 000000000..b0853bc55 --- /dev/null +++ b/vendor/computer-use/patches/0004-copilot-prompts.patch @@ -0,0 +1,306 @@ +diff --git a/apps/OpenComputerUseLinux/main.go b/apps/OpenComputerUseLinux/main.go +index 8634387..552cd56 100644 +--- a/apps/OpenComputerUseLinux/main.go ++++ b/apps/OpenComputerUseLinux/main.go +@@ -27,7 +27,7 @@ var clickMethodValues = []string{"auto", "accessibility", "app_post", "sky_click + //go:embed runtime.py + var linuxRuntimeScript string + +-const serverInstructions = "Computer Use tools let you interact with Linux desktop apps by performing UI actions.\n\nBegin by calling `get_app_state` every turn you want to use Computer Use to get the latest state before acting. The available tools are list_apps, get_app_state, click, perform_secondary_action, scroll, drag, type_text, press_key, set_value, and screen_capture.\n\nPrefer element-targeted interactions over coordinate clicks when an index for the targeted element is available. Linux actions use AT-SPI2 semantic actions and editable text APIs first. Coordinate mouse and key synthesis are best-effort fallbacks and are not a universal Wayland background input model." ++const serverInstructions = "Biorouter Copilot tools let you interact with Linux desktop apps by performing UI actions.\n\nBegin by calling `get_app_state` every turn you want to use Biorouter Copilot to get the latest state before acting. The available tools are list_apps, get_app_state, click, perform_secondary_action, scroll, drag, type_text, press_key, set_value, and screen_capture.\n\nPrefer element-targeted interactions over coordinate clicks when an index for the targeted element is available. Linux actions use AT-SPI2 semantic actions and editable text APIs first. Coordinate mouse and key synthesis are best-effort fallbacks and are not a universal Wayland background input model." + + type toolDefinition struct { + Name string `json:"name"` +@@ -1242,7 +1242,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "click", +- Description: "Click an element by index or pixel coordinates from screenshot. This tool is part of plugin `Computer Use`.", ++ Description: "Click an element by index or pixel coordinates from screenshot. This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -1256,7 +1256,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "drag", +- Description: "Drag from one point to another using pixel coordinates. This tool is part of plugin `Computer Use`.", ++ Description: "Drag from one point to another using pixel coordinates. This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -1268,7 +1268,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "get_app_state", +- Description: "Get the state of an already running app's key window and return a screenshot and accessibility tree. This must be called once per assistant turn before interacting with the app. This tool is part of plugin `Computer Use`.", ++ Description: "Get the state of an already running app's key window and return a screenshot and accessibility tree. This must be called once per assistant turn before interacting with the app. This tool is part of Biorouter Copilot.", + Annotations: readOnlyAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -1279,13 +1279,13 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "list_apps", +- Description: "List the apps on this computer. Returns the set of apps that are currently running, as well as any that have been used in the last 14 days, including details on usage frequency. This tool is part of plugin `Computer Use`.", ++ Description: "List the apps on this computer. Returns the set of apps that are currently running, as well as any that have been used in the last 14 days, including details on usage frequency. This tool is part of Biorouter Copilot.", + Annotations: readOnlyAnnotations(), + InputSchema: objectSchema(map[string]any{}, nil), + }, + { + Name: "perform_secondary_action", +- Description: "Invoke a secondary accessibility action exposed by an element. This tool is part of plugin `Computer Use`.", ++ Description: "Invoke a secondary accessibility action exposed by an element. This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -1295,7 +1295,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "press_key", +- Description: "Press a key or key-combination on the keyboard, including modifier and navigation keys.\n - This supports xdotool's `key` syntax.\n - Examples: \"a\", \"Return\", \"Tab\", \"super+c\", \"Up\", \"KP_0\" (for the numpad 0). This tool is part of plugin `Computer Use`.", ++ Description: "Press a key or key-combination on the keyboard, including modifier and navigation keys.\n - This supports xdotool's `key` syntax.\n - Examples: \"a\", \"Return\", \"Tab\", \"super+c\", \"Up\", \"KP_0\" (for the numpad 0). This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -1304,7 +1304,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "scroll", +- Description: "Scroll an element in a direction by a number of pages. This tool is part of plugin `Computer Use`.", ++ Description: "Scroll an element in a direction by a number of pages. This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -1315,7 +1315,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "set_value", +- Description: "Set the value of a settable accessibility element. This tool is part of plugin `Computer Use`.", ++ Description: "Set the value of a settable accessibility element. This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -1325,7 +1325,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "type_text", +- Description: "Type literal text using keyboard input. This tool is part of plugin `Computer Use`.", ++ Description: "Type literal text using keyboard input. This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +diff --git a/apps/OpenComputerUseWindows/main.go b/apps/OpenComputerUseWindows/main.go +index efba13e..0efe82f 100644 +--- a/apps/OpenComputerUseWindows/main.go ++++ b/apps/OpenComputerUseWindows/main.go +@@ -24,7 +24,7 @@ var clickMethodValues = []string{"auto", "accessibility", "app_post", "sky_click + //go:embed runtime.ps1 + var windowsRuntimeScript string + +-const serverInstructions = "Computer Use tools let you interact with Windows apps by performing UI actions.\n\nBegin by calling `get_app_state` every turn you want to use Computer Use to get the latest state before acting. The available tools are list_apps, get_app_state, click, perform_secondary_action, scroll, drag, type_text, press_key, set_value, and screen_capture.\n\nPrefer element-targeted interactions over coordinate clicks when an index for the targeted element is available. Windows actions use UI Automation patterns first and fall back to window messages when an app does not expose the needed pattern. The Windows runtime does not auto-launch apps, perform SetFocus, or use UIA text fallback by default, so background-capable actions do not intentionally steal the user's foreground focus." ++const serverInstructions = "Biorouter Copilot tools let you interact with Windows apps by performing UI actions.\n\nBegin by calling `get_app_state` every turn you want to use Biorouter Copilot to get the latest state before acting. The available tools are list_apps, get_app_state, click, perform_secondary_action, scroll, drag, type_text, press_key, set_value, and screen_capture.\n\nPrefer element-targeted interactions over coordinate clicks when an index for the targeted element is available. Windows actions use UI Automation patterns first and fall back to window messages when an app does not expose the needed pattern. The Windows runtime does not auto-launch apps, perform SetFocus, or use UIA text fallback by default, so background-capable actions do not intentionally steal the user's foreground focus." + + type toolDefinition struct { + Name string `json:"name"` +@@ -815,7 +815,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "click", +- Description: "Click an element by index or pixel coordinates from screenshot. This tool is part of plugin `Computer Use`.", ++ Description: "Click an element by index or pixel coordinates from screenshot. This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -829,7 +829,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "drag", +- Description: "Drag within the selected window using screenshot-relative pixel coordinates. Requires OPEN_COMPUTER_USE_ALLOW_GLOBAL_POINTER_FALLBACKS=1, moves the system pointer, and focuses the target window. Background drag is unsupported. This tool is part of plugin `Computer Use`.", ++ Description: "Drag within the selected window using screenshot-relative pixel coordinates. Requires the host to allow global pointer fallback, moves the system pointer, and focuses the target window. Background drag is unsupported. This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -841,7 +841,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "get_app_state", +- Description: "Get the state of an already running app's key window and return a screenshot and accessibility tree. This must be called once per assistant turn before interacting with the app. This tool is part of plugin `Computer Use`.", ++ Description: "Get the state of an already running app's key window and return a screenshot and accessibility tree. This must be called once per assistant turn before interacting with the app. This tool is part of Biorouter Copilot.", + Annotations: readOnlyAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -852,13 +852,13 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "list_apps", +- Description: "List the apps on this computer. Returns the set of apps that are currently running, as well as any that have been used in the last 14 days, including details on usage frequency. This tool is part of plugin `Computer Use`.", ++ Description: "List the apps on this computer. Returns the set of apps that are currently running, as well as any that have been used in the last 14 days, including details on usage frequency. This tool is part of Biorouter Copilot.", + Annotations: readOnlyAnnotations(), + InputSchema: objectSchema(map[string]any{}, nil), + }, + { + Name: "perform_secondary_action", +- Description: "Invoke a secondary accessibility action exposed by an element. This tool is part of plugin `Computer Use`.", ++ Description: "Invoke a secondary accessibility action exposed by an element. This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -868,7 +868,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "press_key", +- Description: "Press a key or key-combination on the keyboard, including modifier and navigation keys.\n - This supports xdotool's `key` syntax.\n - Examples: \"a\", \"Return\", \"Tab\", \"super+c\", \"Up\", \"KP_0\" (for the numpad 0). This tool is part of plugin `Computer Use`.", ++ Description: "Press a key or key-combination on the keyboard, including modifier and navigation keys.\n - This supports xdotool's `key` syntax.\n - Examples: \"a\", \"Return\", \"Tab\", \"super+c\", \"Up\", \"KP_0\" (for the numpad 0). This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -877,7 +877,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "scroll", +- Description: "Scroll an element in a direction by a number of pages. This tool is part of plugin `Computer Use`.", ++ Description: "Scroll an element in a direction by a number of pages. This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -888,7 +888,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "set_value", +- Description: "Set the value of a settable accessibility element. This tool is part of plugin `Computer Use`.", ++ Description: "Set the value of a settable accessibility element. This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +@@ -898,7 +898,7 @@ func toolDefinitions() []toolDefinition { + }, + { + Name: "type_text", +- Description: "Type literal text using keyboard input. This tool is part of plugin `Computer Use`.", ++ Description: "Type literal text using keyboard input. This tool is part of Biorouter Copilot.", + Annotations: defaultAnnotations(), + InputSchema: objectSchema(map[string]any{ + "app": stringProperty("App name or bundle identifier"), +diff --git a/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/MCPServer.swift b/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/MCPServer.swift +index 93491ca..cda0291 100644 +--- a/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/MCPServer.swift ++++ b/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/MCPServer.swift +@@ -1,19 +1,19 @@ + import Foundation + + let computerUseServerInstructions = """ +-Computer Use tools let you interact with macOS apps by performing UI actions. ++Biorouter Copilot tools let you interact with macOS apps by performing UI actions. + +-Some apps might have a separate dedicated plugin or skill. You may want to use that plugin or skill instead of Computer Use when it seems like a good fit for the task. While the separate plugin or skill may not expose every feature in the app, if the plugin can perform the task with its available features, prefer it. If the needed capability is not exposed there, use Computer Use may be appropriate for the missing interaction. ++Some apps might have a separate dedicated plugin or skill. You may want to use that plugin or skill instead of Biorouter Copilot when it seems like a good fit for the task. While the separate plugin or skill may not expose every feature in the app, if the plugin can perform the task with its available features, prefer it. If the needed capability is not exposed there, Biorouter Copilot may be appropriate for the missing interaction. + +-Begin by calling `get_app_state` every turn you want to use Computer Use to get the latest state before acting. Codex will automatically stop the session after each assistant turn, so this step is required before interacting with apps in a new assistant turn. ++Begin by calling `get_app_state` every turn you want to use Biorouter Copilot to get the latest state before acting. Call it again in a new assistant turn before interacting with apps. + +-The available tools are list_apps, get_app_state, click, perform_secondary_action, scroll, drag, type_text, press_key, set_value, and screen_capture. If any of these are not available in your environment, use tool_search to surface one before calling any Computer Use action tools. ++The available tools are list_apps, get_app_state, click, perform_secondary_action, scroll, drag, type_text, press_key, set_value, and screen_capture. If any of these are not available in your environment, use tool_search to surface one before calling any Biorouter Copilot action tools. + +-Computer Use tools allow you to use the user's apps in the background, so while you're using an app, the user can continue to use other apps on their computer. Avoid doing anything that would disrupt the user's active session, such as overwriting the contents of their clipboard, unless they asked you to! ++Biorouter Copilot tools allow you to use the user's apps in the background, so while you're using an app, the user can continue to use other apps on their computer. Avoid doing anything that would disrupt the user's active session, such as overwriting the contents of their clipboard, unless they asked you to! + + After each action, use the action result or fetch the latest state to verify the UI changed as expected. + Prefer element-targeted interactions over coordinate clicks when an index for the targeted element is available. Note that element indices are the sequential integers from the app state's accessibility tree. +-Avoid falling back to AppleScript during a computer use session. Prefer Computer Use tools as much as possible to complete tasks. ++Avoid falling back to AppleScript during a Copilot session. Prefer Biorouter Copilot tools as much as possible to complete tasks. + Ask the user before taking destructive or externally visible actions such as sending, deleting, or purchasing. If helpful, you can ask follow-up questions before taking action to make sure you’re understanding the user’s request correctly. + """ + +diff --git a/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/ToolDefinitions.swift b/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/ToolDefinitions.swift +index c0e7856..a05f47b 100644 +--- a/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/ToolDefinitions.swift ++++ b/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/ToolDefinitions.swift +@@ -42,7 +42,7 @@ public enum ToolDefinitions { + ), + ToolDefinition( + name: "click", +- description: "Click an element by index or pixel coordinates from screenshot. This tool is part of plugin `Computer Use`.", ++ description: "Click an element by index or pixel coordinates from screenshot. This tool is part of Biorouter Copilot.", + annotations: defaultAnnotations(), + inputSchema: objectSchema( + properties: [ +@@ -56,7 +56,7 @@ public enum ToolDefinitions { + enumValues: ["left", "right", "middle"] + ), + "click_method": stringProperty( +- description: "Click implementation: auto (default), accessibility, app_post, sky_click, or global. Accessibility requires element_index. app_post sends a public event directly to the target app. sky_click uses the macOS SkyLight background window path. Global may move the system pointer and requires OPEN_COMPUTER_USE_ALLOW_GLOBAL_POINTER_FALLBACKS=1.", ++ description: "Click implementation: auto (default), accessibility, app_post, sky_click, or global. Accessibility requires element_index. app_post sends a public event directly to the target app. sky_click uses the macOS SkyLight background window path. Global may move the system pointer and requires the host to allow global pointer fallback.", + enumValues: ClickMethod.allCases.map(\.rawValue) + ), + ], +@@ -65,7 +65,7 @@ public enum ToolDefinitions { + ), + ToolDefinition( + name: "drag", +- description: "Drag from one point to another using pixel coordinates. By default mouse events are posted directly to the target app and the system pointer does not move; that path cannot drive window-server drag sessions such as window moves, text selection, or Finder drag-and-drop. Those require OPEN_COMPUTER_USE_ALLOW_GLOBAL_POINTER_FALLBACKS=1 in the server process environment, which may move the real pointer. The result reports which path was used. This tool is part of plugin `Computer Use`.", ++ description: "Drag from one point to another using pixel coordinates. By default mouse events are posted directly to the target app and the system pointer does not move; that path cannot drive window-server drag sessions such as window moves, text selection, or Finder drag-and-drop. Those require the host to allow global pointer fallback, which may move the real pointer. The result reports which path was used. This tool is part of Biorouter Copilot.", + annotations: defaultAnnotations(), + inputSchema: objectSchema( + properties: [ +@@ -80,7 +80,7 @@ public enum ToolDefinitions { + ), + ToolDefinition( + name: "get_app_state", +- description: "Start an app use session if needed, then get the state of the app's key window and return a screenshot and accessibility tree. This must be called once per assistant turn before interacting with the app. This tool is part of plugin `Computer Use`.", ++ description: "Start an app use session if needed, then get the state of the app's key window and return a screenshot and accessibility tree. This must be called once per assistant turn before interacting with the app. This tool is part of Biorouter Copilot.", + annotations: readOnlyAnnotations(), + inputSchema: objectSchema( + properties: [ +@@ -94,13 +94,13 @@ public enum ToolDefinitions { + ), + ToolDefinition( + name: "list_apps", +- description: "List the apps on this computer. Returns the set of apps that are currently running, as well as any that have been used in the last 14 days, including details on usage frequency. This tool is part of plugin `Computer Use`.", ++ description: "List the apps on this computer. Returns the set of apps that are currently running, as well as any that have been used in the last 14 days, including details on usage frequency. This tool is part of Biorouter Copilot.", + annotations: readOnlyAnnotations(), + inputSchema: objectSchema(properties: [:], required: []) + ), + ToolDefinition( + name: "perform_secondary_action", +- description: "Invoke a secondary accessibility action exposed by an element. This tool is part of plugin `Computer Use`.", ++ description: "Invoke a secondary accessibility action exposed by an element. This tool is part of Biorouter Copilot.", + annotations: defaultAnnotations(), + inputSchema: objectSchema( + properties: [ +@@ -113,7 +113,7 @@ public enum ToolDefinitions { + ), + ToolDefinition( + name: "press_key", +- description: "Press a key or key-combination on the keyboard, including modifier and navigation keys.\n - This supports xdotool's `key` syntax.\n - Examples: \"a\", \"Return\", \"Tab\", \"super+c\", \"Up\", \"KP_0\" (for the numpad 0 key). This tool is part of plugin `Computer Use`.", ++ description: "Press a key or key-combination on the keyboard, including modifier and navigation keys.\n - This supports xdotool's `key` syntax.\n - Examples: \"a\", \"Return\", \"Tab\", \"super+c\", \"Up\", \"KP_0\" (for the numpad 0 key). This tool is part of Biorouter Copilot.", + annotations: defaultAnnotations(), + inputSchema: objectSchema( + properties: [ +@@ -125,7 +125,7 @@ public enum ToolDefinitions { + ), + ToolDefinition( + name: "scroll", +- description: "Scroll an element in a direction by a number of pages. This tool is part of plugin `Computer Use`.", ++ description: "Scroll an element in a direction by a number of pages. This tool is part of Biorouter Copilot.", + annotations: defaultAnnotations(), + inputSchema: objectSchema( + properties: [ +@@ -139,7 +139,7 @@ public enum ToolDefinitions { + ), + ToolDefinition( + name: "set_value", +- description: "Set the value of a settable accessibility element. This tool is part of plugin `Computer Use`.", ++ description: "Set the value of a settable accessibility element. This tool is part of Biorouter Copilot.", + annotations: defaultAnnotations(), + inputSchema: objectSchema( + properties: [ +@@ -152,7 +152,7 @@ public enum ToolDefinitions { + ), + ToolDefinition( + name: "type_text", +- description: "Type literal text using keyboard input. This tool is part of plugin `Computer Use`.", ++ description: "Type literal text using keyboard input. This tool is part of Biorouter Copilot.", + annotations: defaultAnnotations(), + inputSchema: objectSchema( + properties: [ diff --git a/vendor/computer-use/patches/0005-copilot-accessibility-guidance.patch b/vendor/computer-use/patches/0005-copilot-accessibility-guidance.patch new file mode 100644 index 000000000..c5b02d909 --- /dev/null +++ b/vendor/computer-use/patches/0005-copilot-accessibility-guidance.patch @@ -0,0 +1,13 @@ +diff --git a/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/AccessibilitySnapshot.swift b/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/AccessibilitySnapshot.swift +index a91ac94..02b2487 100644 +--- a/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/AccessibilitySnapshot.swift ++++ b/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/AccessibilitySnapshot.swift +@@ -161,7 +161,7 @@ enum SnapshotBuilder { + + let permissions = PermissionDiagnostics.current() + guard permissions.accessibilityTrusted else { +- throw ComputerUseError.permissionDenied("Accessibility permission is required. Run `open-computer-use doctor` and grant access to Open Computer Use.") ++ throw ComputerUseError.permissionDenied("Biorouter Copilot requires Accessibility permission. In System Settings, allow \(PermissionSupport.currentBundleDisplayName()) under Accessibility.") + } + + try ComputerUseCancellation.check() diff --git a/vendor/computer-use/pin.json b/vendor/computer-use/pin.json index d614a5ad5..5be67b598 100644 --- a/vendor/computer-use/pin.json +++ b/vendor/computer-use/pin.json @@ -3,7 +3,7 @@ "repository": "https://github.com/iFurySt/open-codex-computer-use.git", "upstream_commit": "547b4ffb8ed731a8f16486e6d8a3b215484267d3", "upstream_version": "0.3.5", - "patch_revision": "biorouter-1", + "patch_revision": "biorouter-3", "targets": [ "darwin-arm64", "darwin-x64",