From 26cb1829f275368f66ec34412b220c5e57e756fc Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 16:01:39 -0700 Subject: [PATCH 01/21] docs(release): draft v1.91.2 notes --- docs/releases/notes/v1.91.2.md | 47 ++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/releases/notes/v1.91.2.md diff --git a/docs/releases/notes/v1.91.2.md b/docs/releases/notes/v1.91.2.md new file mode 100644 index 000000000..d628b3555 --- /dev/null +++ b/docs/releases/notes/v1.91.2.md @@ -0,0 +1,47 @@ +# 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. + +## 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. From 7ff03c015ab971a5ad2f4146cb45954b8d967d47 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 16:01:35 -0700 Subject: [PATCH 02/21] fix(copilot): use Copilot wording in prompts and slash references --- biorouter-self-test.yaml | 2 +- .../biorouter-cli/src/session/completion.rs | 18 +++++++++++-- crates/biorouter-cli/src/session/tui/mod.rs | 6 ++--- .../builtin_skills/about-biorouter/SKILL.md | 2 +- .../builtin_skills/develop-biorouter/SKILL.md | 2 +- .../biorouter/src/agents/execute_commands.rs | 26 ++++++++++++------ .../biorouter/src/agents/extension_manager.rs | 27 +++++++++++++++++++ crates/biorouter/src/prompts/system.md | 2 +- scripts/check-computer-use-naming.sh | 19 ++++++++++--- 9 files changed, 83 insertions(+), 21 deletions(-) diff --git a/biorouter-self-test.yaml b/biorouter-self-test.yaml index 9fe7e8f12..c6781e3bd 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. 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/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..1a337a8dd 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,12 @@ pub(crate) fn exact_custom_reference_key( target: Option<&BundledExtensionTarget>, active: &[ExtensionConfig], ) -> Option { + // The compact CLI alias must not resolve to an unrelated active extension. + if requested.eq_ignore_ascii_case(COPILOT_REFERENCE_ALIAS) + && target.is_some_and(|target| target.key() == "computercontroller") + { + return None; + } active .iter() .find(|config| { @@ -5549,6 +5557,14 @@ mod tests { 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 + ); let target = resolve_bundled_extension("developer").unwrap(); assert_eq!( exact_custom_reference_key("developer", Some(&target), &[custom("developer")]), @@ -5573,6 +5589,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/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/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 From 78c0a49a926dcb5df68ad86f9395a86c1d503f91 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 16:07:01 -0700 Subject: [PATCH 03/21] fix(copilot): hide Windows PowerShell children and rename helper prompts --- .github/workflows/computer-use-native.yml | 7 +- biorouter-self-test.yaml | 7 +- crates/biorouter-sandbox/src/console.rs | 10 +- scripts/test-computer-use-protocol.py | 7 + scripts/test-computer-use-windows-console.py | 225 +++++++++++++ ui/desktop/scripts/console-window-census.mjs | 9 +- ui/desktop/scripts/windows-console.test.mjs | 7 +- .../patches/0003-windows-console.patch | 76 +++++ .../patches/0004-copilot-prompts.patch | 297 ++++++++++++++++++ vendor/computer-use/pin.json | 2 +- 10 files changed, 630 insertions(+), 17 deletions(-) create mode 100644 scripts/test-computer-use-windows-console.py create mode 100644 vendor/computer-use/patches/0003-windows-console.patch create mode 100644 vendor/computer-use/patches/0004-copilot-prompts.patch diff --git a/.github/workflows/computer-use-native.yml b/.github/workflows/computer-use-native.yml index 36ad7d0da..0eb8593c8 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 pre-fix 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/biorouter-self-test.yaml b/biorouter-self-test.yaml index c6781e3bd..ec6358a85 100644 --- a/biorouter-self-test.yaml +++ b/biorouter-self-test.yaml @@ -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-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/scripts/test-computer-use-protocol.py b/scripts/test-computer-use-protocol.py index b4b98905f..e3745f04e 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"\b(?:open[ -]+computer[ -]+use|computer[ -]+controller|computer[ -]+use)\b", re.I) def semantic_schema(value): @@ -27,6 +29,8 @@ 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"]) + if RETIRED_PROMPT_NAME.search(actual.get("description", "")): + 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 +54,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..5d495bd72 --- /dev/null +++ b/scripts/test-computer-use-windows-console.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Check the Windows Copilot helper's PowerShell child from a GUI process.""" + +import argparse +import ctypes +from ctypes import wintypes +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import time + + +ROOT = Path(__file__).resolve().parents[1] +PATCH = ROOT / "vendor/computer-use/patches/0003-windows-console.patch" +CREATE_NO_WINDOW = 0x08000000 +PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 +SYNCHRONIZE = 0x00100000 +WAIT_TIMEOUT = 0x00000102 +TH32CS_SNAPPROCESS = 0x00000002 + + +class ProcessEntry(ctypes.Structure): + _fields_ = [ + ("dwSize", wintypes.DWORD), + ("cntUsage", wintypes.DWORD), + ("th32ProcessID", wintypes.DWORD), + ("th32DefaultHeapID", ctypes.c_void_p), + ("th32ModuleID", wintypes.DWORD), + ("cntThreads", wintypes.DWORD), + ("th32ParentProcessID", wintypes.DWORD), + ("pcPriClassBase", wintypes.LONG), + ("dwFlags", wintypes.DWORD), + ("szExeFile", wintypes.WCHAR * 260), + ] + + +def windows_api(): + if os.name != "nt": + raise RuntimeError("the console probe requires Windows") + kernel = ctypes.WinDLL("kernel32", use_last_error=True) + user = ctypes.WinDLL("user32", use_last_error=True) + kernel.CreateToolhelp32Snapshot.argtypes = [wintypes.DWORD, wintypes.DWORD] + kernel.CreateToolhelp32Snapshot.restype = wintypes.HANDLE + kernel.Process32FirstW.argtypes = [wintypes.HANDLE, ctypes.POINTER(ProcessEntry)] + kernel.Process32NextW.argtypes = [wintypes.HANDLE, ctypes.POINTER(ProcessEntry)] + kernel.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] + kernel.OpenProcess.restype = wintypes.HANDLE + kernel.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD] + kernel.CloseHandle.argtypes = [wintypes.HANDLE] + kernel.AttachConsole.argtypes = [wintypes.DWORD] + kernel.GetConsoleWindow.restype = wintypes.HWND + user.IsWindowVisible.argtypes = [wintypes.HWND] + return kernel, user + + +def child_powershell_pids(kernel, parent_pid): + snapshot = kernel.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) + if snapshot == ctypes.c_void_p(-1).value: + raise ctypes.WinError(ctypes.get_last_error()) + found = [] + try: + entry = ProcessEntry() + entry.dwSize = ctypes.sizeof(entry) + ok = kernel.Process32FirstW(snapshot, ctypes.byref(entry)) + while ok: + if entry.th32ParentProcessID == parent_pid and entry.szExeFile.lower() == "powershell.exe": + found.append(entry.th32ProcessID) + ok = kernel.Process32NextW(snapshot, ctypes.byref(entry)) + finally: + kernel.CloseHandle(snapshot) + return found + + +def console_state(kernel, user, pid): + process = kernel.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, False, pid) + if not process: + return None + try: + if kernel.WaitForSingleObject(process, 0) != WAIT_TIMEOUT: + return None + kernel.FreeConsole() + if not kernel.AttachConsole(pid): + error = ctypes.get_last_error() + if error == 6: # ERROR_INVALID_HANDLE: the child has no console. + return "none" + raise ctypes.WinError(error) + try: + window = kernel.GetConsoleWindow() + return "visible" if window and user.IsWindowVisible(window) else "hidden" + finally: + kernel.FreeConsole() + finally: + kernel.CloseHandle(process) + + +def watch(kernel, user, process, child_of=None, timeout=35): + states = [] + children = set() + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + pids = [process.pid] if child_of is None else child_powershell_pids(kernel, child_of) + for pid in pids: + state = console_state(kernel, user, pid) + if state is not None: + children.add(pid) + states.append(state) + if process.poll() is not None: + break + time.sleep(0.01) + if process.poll() is None: + process.kill() + raise TimeoutError(f"process {process.pid} did not finish") + if not states: + raise AssertionError(f"no live console samples from process {process.pid}") + return {"parent_pid": child_of, "child_pids": sorted(children), "states": sorted(set(states))} + + +def run_helper(kernel, user, helper, label, work): + 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"], + stdout=stdout, + stderr=stderr, + creationflags=CREATE_NO_WINDOW, + env=dict(os.environ, OPEN_COMPUTER_USE_DISABLE_APP_AGENT_PROXY="1"), + ) + observation = watch(kernel, user, process, child_of=process.pid) + code = process.wait(timeout=5) + if code: + raise AssertionError(f"{label} UIA call failed: exit {code}, stderr={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 observation + + +def observe(patched, baseline): + kernel, user = windows_api() + if kernel.GetConsoleWindow(): + raise AssertionError("observer has a console; pythonw.exe must be the GUI parent") + with tempfile.TemporaryDirectory(prefix="biorouter-console-observer-") as temp: + work = Path(temp) + with (work / "control.stdout").open("wb") as stdout: + control = subprocess.Popen( + ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", "Start-Sleep -Seconds 3"], + stdout=stdout, + stderr=subprocess.STDOUT, + ) + positive = watch(kernel, user, control, timeout=8) + control.wait(timeout=5) + if "visible" not in positive["states"]: + raise AssertionError(f"unflagged PowerShell positive control was not visible: {positive}") + before = run_helper(kernel, user, baseline, "before", work) + after = run_helper(kernel, user, patched, "after", work) + if "visible" not in before["states"]: + raise AssertionError(f"pre-fix helper did not reproduce the visible child: {before}") + if "visible" in after["states"]: + raise AssertionError(f"patched helper still showed a PowerShell console: {after}") + return {"positive_control": positive, "pre_fix_helper": before, "patched_helper": after} + + +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 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) + parser.add_argument("--worker", action="store_true") + parser.add_argument("--baseline", type=Path) + args = parser.parse_args() + if args.worker: + try: + result = observe(args.patched, args.baseline) + except Exception as error: + result = {"error": f"{type(error).__name__}: {error}"} + args.report.write_text(json.dumps(result, indent=2), encoding="utf-8") + return 1 if "error" in result else 0 + if os.name != "nt": + parser.error("the console probe requires Windows") + pythonw = Path(sys.executable).with_name("pythonw.exe") + if not pythonw.is_file(): + raise FileNotFoundError(f"GUI-subsystem Python is unavailable: {pythonw}") + args.report.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="biorouter-console-baseline-") as temp: + baseline = Path(temp) / "ocu-before-console-fix.exe" + build_baseline(args.source.resolve(), baseline) + completed = subprocess.run( + [str(pythonw), __file__, "--worker", "--baseline", str(baseline), + "--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/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..c83b1e71b --- /dev/null +++ b/vendor/computer-use/patches/0004-copilot-prompts.patch @@ -0,0 +1,297 @@ +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..c602987 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 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 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..88a5d61 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, use 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. Codex will automatically stop the session after each assistant turn, so this step is required before interacting with apps in a new assistant turn. + +-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 computer use 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..70da08f 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: [ +@@ -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 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 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/pin.json b/vendor/computer-use/pin.json index d614a5ad5..ba934c202 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-2", "targets": [ "darwin-arm64", "darwin-x64", From dc8ba60951cf171b02cb86086a04432705200b74 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 16:08:16 -0700 Subject: [PATCH 04/21] fix(copilot): reserve every Copilot slash spelling --- crates/biorouter/src/agents/extension_manager.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/biorouter/src/agents/extension_manager.rs b/crates/biorouter/src/agents/extension_manager.rs index 1a337a8dd..625ed2c94 100644 --- a/crates/biorouter/src/agents/extension_manager.rs +++ b/crates/biorouter/src/agents/extension_manager.rs @@ -796,8 +796,8 @@ pub(crate) fn exact_custom_reference_key( target: Option<&BundledExtensionTarget>, active: &[ExtensionConfig], ) -> Option { - // The compact CLI alias must not resolve to an unrelated active extension. - if requested.eq_ignore_ascii_case(COPILOT_REFERENCE_ALIAS) + // Every spelling of the Copilot reference must retain the bundled target. + if extension_reference_key(requested) == extension_reference_key(COPILOT_REFERENCE_ALIAS) && target.is_some_and(|target| target.key() == "computercontroller") { return None; @@ -5565,6 +5565,14 @@ mod tests { ), None ); + assert_eq!( + exact_custom_reference_key( + "Biorouter Copilot", + Some(&target), + &[custom("Biorouter Copilot")] + ), + None + ); let target = resolve_bundled_extension("developer").unwrap(); assert_eq!( exact_custom_reference_key("developer", Some(&target), &[custom("developer")]), From d4a99ee446afd066e1a78ca7c057df9f973f10b7 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 16:08:27 -0700 Subject: [PATCH 05/21] chore(release): bump version to 1.91.2 --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- README.md | 2 +- ui/desktop/openapi.json | 2 +- ui/desktop/package-lock.json | 4 ++-- ui/desktop/package.json | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) 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/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" From fb9221e77fdbf7cf005a868088ece2c26415b509 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 16:10:04 -0700 Subject: [PATCH 06/21] fix(copilot): polish native helper prompts --- scripts/test-computer-use-protocol.py | 17 +++++++++++-- .../patches/0004-copilot-prompts.patch | 25 +++++++++++++------ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/scripts/test-computer-use-protocol.py b/scripts/test-computer-use-protocol.py index e3745f04e..e154c6f32 100644 --- a/scripts/test-computer-use-protocol.py +++ b/scripts/test-computer-use-protocol.py @@ -10,7 +10,7 @@ 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"\b(?:open[ -]+computer[ -]+use|computer[ -]+controller|computer[ -]+use)\b", re.I) +RETIRED_PROMPT_NAME = re.compile(r"(?:open[ _-]*computer[ _-]*use|computer[ _-]+controller|computer[ _-]+use)(?![a-z])", re.I) def semantic_schema(value): @@ -22,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} @@ -29,7 +41,8 @@ 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"]) - if RETIRED_PROMPT_NAME.search(actual.get("description", "")): + 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) diff --git a/vendor/computer-use/patches/0004-copilot-prompts.patch b/vendor/computer-use/patches/0004-copilot-prompts.patch index c83b1e71b..b0853bc55 100644 --- a/vendor/computer-use/patches/0004-copilot-prompts.patch +++ b/vendor/computer-use/patches/0004-copilot-prompts.patch @@ -91,7 +91,7 @@ index 8634387..552cd56 100644 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..c602987 100644 +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 @@ -117,7 +117,7 @@ index efba13e..c602987 100644 { 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 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 Biorouter Copilot.", ++ 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"), @@ -183,7 +183,7 @@ index efba13e..c602987 100644 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..88a5d61 100644 +index 93491ca..cda0291 100644 --- a/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/MCPServer.swift +++ b/packages/OpenComputerUseKit/Sources/OpenComputerUseKit/MCPServer.swift @@ -1,19 +1,19 @@ @@ -194,10 +194,10 @@ index 93491ca..88a5d61 100644 +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, use Biorouter Copilot 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. 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. @@ -208,12 +208,12 @@ index 93491ca..88a5d61 100644 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 computer use session. Prefer Biorouter Copilot 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..70da08f 100644 +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 { @@ -225,12 +225,21 @@ index c0e7856..70da08f 100644 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 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 Biorouter Copilot.", ++ 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: [ From 90548afa355280f848a4c5552c426fddaa105a64 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 16:13:57 -0700 Subject: [PATCH 07/21] test(copilot): reject compact retired names in helper prompts --- scripts/test-computer-use-protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test-computer-use-protocol.py b/scripts/test-computer-use-protocol.py index e154c6f32..f38f3c1f1 100644 --- a/scripts/test-computer-use-protocol.py +++ b/scripts/test-computer-use-protocol.py @@ -10,7 +10,7 @@ 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) +RETIRED_PROMPT_NAME = re.compile(r"(?:open[ _-]*computer[ _-]*use|computer[ _-]*controller|computer[ _-]*use)(?![a-z])", re.I) def semantic_schema(value): From 53e8c34a2631c68610a319cd80171d16226ea8b6 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 16:27:54 -0700 Subject: [PATCH 08/21] test(copilot): reserve built-in slash reference over custom alias --- crates/biorouter/src/agents/extension_manager.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/biorouter/src/agents/extension_manager.rs b/crates/biorouter/src/agents/extension_manager.rs index 625ed2c94..326601269 100644 --- a/crates/biorouter/src/agents/extension_manager.rs +++ b/crates/biorouter/src/agents/extension_manager.rs @@ -5534,7 +5534,7 @@ mod tests { } #[test] - fn exact_active_custom_alias_is_not_redirected_to_a_builtin() { + fn copilot_reference_is_reserved_even_when_custom_name_matches() { let custom = |name: &str| ExtensionConfig::Frontend { name: name.to_string(), description: String::new(), @@ -5551,7 +5551,7 @@ mod tests { Some(&target), std::slice::from_ref(&entry) ), - Some(entry.key()) + None ); assert_eq!( exact_custom_reference_key("computercontroller", Some(&target), &[entry]), From 4e540732c96d1af7c473bf7d0ec99fcb221d750a Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 16:50:41 -0700 Subject: [PATCH 09/21] test(windows): report console probe traceback on failure --- scripts/test-computer-use-windows-console.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/test-computer-use-windows-console.py b/scripts/test-computer-use-windows-console.py index 5d495bd72..0e21c12ec 100644 --- a/scripts/test-computer-use-windows-console.py +++ b/scripts/test-computer-use-windows-console.py @@ -12,6 +12,7 @@ import sys import tempfile import time +import traceback ROOT = Path(__file__).resolve().parents[1] @@ -192,7 +193,10 @@ def main(): try: result = observe(args.patched, args.baseline) except Exception as error: - result = {"error": f"{type(error).__name__}: {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 if os.name != "nt": From 56705304c02702c126544bcb099c5e52c9fe4503 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 17:10:28 -0700 Subject: [PATCH 10/21] test: update copilot prompt snapshots --- .../biorouter__agents__prompt_manager__tests__basic.snap | 2 +- ...biorouter__agents__prompt_manager__tests__one_extension.snap | 2 +- ...biorouter__agents__prompt_manager__tests__typical_setup.snap | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 From 065f09a8395a4c7ed49d427293e7f315bf7fad78 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 17:10:49 -0700 Subject: [PATCH 11/21] test(windows): measure PowerShell child from inside console probe --- scripts/test-computer-use-windows-console.py | 340 ++++++++++--------- 1 file changed, 184 insertions(+), 156 deletions(-) diff --git a/scripts/test-computer-use-windows-console.py b/scripts/test-computer-use-windows-console.py index 0e21c12ec..a63522abf 100644 --- a/scripts/test-computer-use-windows-console.py +++ b/scripts/test-computer-use-windows-console.py @@ -1,9 +1,7 @@ #!/usr/bin/env python3 -"""Check the Windows Copilot helper's PowerShell child from a GUI process.""" +"""Compare Windows Copilot helper child-console behavior with its pre-fix build.""" import argparse -import ctypes -from ctypes import wintypes import json import os from pathlib import Path @@ -11,159 +9,82 @@ import subprocess import sys import tempfile -import time import traceback ROOT = Path(__file__).resolve().parents[1] PATCH = ROOT / "vendor/computer-use/patches/0003-windows-console.patch" CREATE_NO_WINDOW = 0x08000000 -PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 -SYNCHRONIZE = 0x00100000 -WAIT_TIMEOUT = 0x00000102 -TH32CS_SNAPPROCESS = 0x00000002 - - -class ProcessEntry(ctypes.Structure): - _fields_ = [ - ("dwSize", wintypes.DWORD), - ("cntUsage", wintypes.DWORD), - ("th32ProcessID", wintypes.DWORD), - ("th32DefaultHeapID", ctypes.c_void_p), - ("th32ModuleID", wintypes.DWORD), - ("cntThreads", wintypes.DWORD), - ("th32ParentProcessID", wintypes.DWORD), - ("pcPriClassBase", wintypes.LONG), - ("dwFlags", wintypes.DWORD), - ("szExeFile", wintypes.WCHAR * 260), - ] - - -def windows_api(): - if os.name != "nt": - raise RuntimeError("the console probe requires Windows") - kernel = ctypes.WinDLL("kernel32", use_last_error=True) - user = ctypes.WinDLL("user32", use_last_error=True) - kernel.CreateToolhelp32Snapshot.argtypes = [wintypes.DWORD, wintypes.DWORD] - kernel.CreateToolhelp32Snapshot.restype = wintypes.HANDLE - kernel.Process32FirstW.argtypes = [wintypes.HANDLE, ctypes.POINTER(ProcessEntry)] - kernel.Process32NextW.argtypes = [wintypes.HANDLE, ctypes.POINTER(ProcessEntry)] - kernel.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] - kernel.OpenProcess.restype = wintypes.HANDLE - kernel.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD] - kernel.CloseHandle.argtypes = [wintypes.HANDLE] - kernel.AttachConsole.argtypes = [wintypes.DWORD] - kernel.GetConsoleWindow.restype = wintypes.HWND - user.IsWindowVisible.argtypes = [wintypes.HWND] - return kernel, user - - -def child_powershell_pids(kernel, parent_pid): - snapshot = kernel.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) - if snapshot == ctypes.c_void_p(-1).value: - raise ctypes.WinError(ctypes.get_last_error()) - found = [] - try: - entry = ProcessEntry() - entry.dwSize = ctypes.sizeof(entry) - ok = kernel.Process32FirstW(snapshot, ctypes.byref(entry)) - while ok: - if entry.th32ParentProcessID == parent_pid and entry.szExeFile.lower() == "powershell.exe": - found.append(entry.th32ProcessID) - ok = kernel.Process32NextW(snapshot, ctypes.byref(entry)) - finally: - kernel.CloseHandle(snapshot) - return found - - -def console_state(kernel, user, pid): - process = kernel.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, False, pid) - if not process: - return None - try: - if kernel.WaitForSingleObject(process, 0) != WAIT_TIMEOUT: - return None - kernel.FreeConsole() - if not kernel.AttachConsole(pid): - error = ctypes.get_last_error() - if error == 6: # ERROR_INVALID_HANDLE: the child has no console. - return "none" - raise ctypes.WinError(error) - try: - window = kernel.GetConsoleWindow() - return "visible" if window and user.IsWindowVisible(window) else "hidden" - finally: - kernel.FreeConsole() - finally: - kernel.CloseHandle(process) - - -def watch(kernel, user, process, child_of=None, timeout=35): - states = [] - children = set() - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - pids = [process.pid] if child_of is None else child_powershell_pids(kernel, child_of) - for pid in pids: - state = console_state(kernel, user, pid) - if state is not None: - children.add(pid) - states.append(state) - if process.poll() is not None: - break - time.sleep(0.01) - if process.poll() is None: - process.kill() - raise TimeoutError(f"process {process.pid} did not finish") - if not states: - raise AssertionError(f"no live console samples from process {process.pid}") - return {"parent_pid": child_of, "child_pids": sorted(children), "states": sorted(set(states))} - - -def run_helper(kernel, user, helper, label, work): - 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"], - stdout=stdout, - stderr=stderr, - creationflags=CREATE_NO_WINDOW, - env=dict(os.environ, OPEN_COMPUTER_USE_DISABLE_APP_AGENT_PROXY="1"), - ) - observation = watch(kernel, user, process, child_of=process.pid) - code = process.wait(timeout=5) - if code: - raise AssertionError(f"{label} UIA call failed: exit {code}, stderr={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 observation +# 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 -def observe(patched, baseline): - kernel, user = windows_api() - if kernel.GetConsoleWindow(): - raise AssertionError("observer has a console; pythonw.exe must be the GUI parent") - with tempfile.TemporaryDirectory(prefix="biorouter-console-observer-") as temp: - work = Path(temp) - with (work / "control.stdout").open("wb") as stdout: - control = subprocess.Popen( - ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", "Start-Sleep -Seconds 3"], - stdout=stdout, - stderr=subprocess.STDOUT, - ) - positive = watch(kernel, user, control, timeout=8) - control.wait(timeout=5) - if "visible" not in positive["states"]: - raise AssertionError(f"unflagged PowerShell positive control was not visible: {positive}") - before = run_helper(kernel, user, baseline, "before", work) - after = run_helper(kernel, user, patched, "after", work) - if "visible" not in before["states"]: - raise AssertionError(f"pre-fix helper did not reproduce the visible child: {before}") - if "visible" in after["states"]: - raise AssertionError(f"patched helper still showed a PowerShell console: {after}") - return {"positive_control": positive, "pre_fix_helper": before, "patched_helper": after} +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "syscall" +) + +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 main() { + report := os.Getenv("BIOROUTER_CONSOLE_PROBE_FILE") + if report == "" { + fmt.Fprintln(os.Stderr, "missing console probe report path") + os.Exit(2) + } + result, err := json.Marshal(map[string]any{ + "verdict": consoleVerdict(), + "pid": os.Getpid(), + }) + 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): @@ -181,17 +102,117 @@ def build_baseline(source, destination): ) +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": + raise AssertionError(f"unflagged control did not show a 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 before["verdict"] != "visible": + raise AssertionError(f"pre-fix helper did not show its PowerShell child: {before}") + if after["verdict"] == "visible": + raise AssertionError(f"patched helper showed its PowerShell child: {after}") + return {"positive_control": positive, "pre_fix_helper": before, "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) + 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) + result = observe(args.patched, args.baseline, args.wrapper, args.real_powershell) except Exception as error: result = { "error": f"{type(error).__name__}: {error}", @@ -199,18 +220,25 @@ def main(): } args.report.write_text(json.dumps(result, indent=2), encoding="utf-8") return 1 if "error" in result else 0 - if os.name != "nt": - parser.error("the console probe requires Windows") + pythonw = Path(sys.executable).with_name("pythonw.exe") if not pythonw.is_file(): raise FileNotFoundError(f"GUI-subsystem Python is unavailable: {pythonw}") - args.report.parent.mkdir(parents=True, exist_ok=True) + 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: - baseline = Path(temp) / "ocu-before-console-fix.exe" + 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), - "--report", str(args.report), str(args.source), str(args.patched.resolve())], + [ + 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, From 78757e8498c53689fc4be97316553d6192f1b7ea Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 17:16:55 -0700 Subject: [PATCH 12/21] test(windows): record pre-fix result without requiring reproduction --- .github/workflows/computer-use-native.yml | 2 +- scripts/test-computer-use-windows-console.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/computer-use-native.yml b/.github/workflows/computer-use-native.yml index 0eb8593c8..10f048c60 100644 --- a/.github/workflows/computer-use-native.yml +++ b/.github/workflows/computer-use-native.yml @@ -59,7 +59,7 @@ 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 pre-fix control + - 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 diff --git a/scripts/test-computer-use-windows-console.py b/scripts/test-computer-use-windows-console.py index a63522abf..2e87e0c1a 100644 --- a/scripts/test-computer-use-windows-console.py +++ b/scripts/test-computer-use-windows-console.py @@ -189,11 +189,14 @@ def observe(patched, baseline, wrapper, real_powershell): 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 before["verdict"] != "visible": - raise AssertionError(f"pre-fix helper did not show its PowerShell child: {before}") if after["verdict"] == "visible": raise AssertionError(f"patched helper showed its PowerShell child: {after}") - return {"positive_control": positive, "pre_fix_helper": before, "patched_helper": after} + return { + "positive_control": positive, + "pre_fix_helper": before, + "pre_fix_reproduced": before["verdict"] == "visible", + "patched_helper": after, + } def main(): From 176a690874d6031a769d6eeff48f150c376c0fdf Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 18:06:16 -0700 Subject: [PATCH 13/21] ci(windows): gate release packages on Copilot console probe --- .github/workflows/windows-gui-packages.yml | 9 +++++++++ 1 file changed, 9 insertions(+) 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 From 43309b79de3030831cbbfb4ae5d170de5aac939d Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 18:07:48 -0700 Subject: [PATCH 14/21] fix(copilot): preserve exact custom extension references --- crates/biorouter/src/agents/extension_manager.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/biorouter/src/agents/extension_manager.rs b/crates/biorouter/src/agents/extension_manager.rs index 326601269..ff0fe5fb6 100644 --- a/crates/biorouter/src/agents/extension_manager.rs +++ b/crates/biorouter/src/agents/extension_manager.rs @@ -796,8 +796,9 @@ pub(crate) fn exact_custom_reference_key( target: Option<&BundledExtensionTarget>, active: &[ExtensionConfig], ) -> Option { - // Every spelling of the Copilot reference must retain the bundled target. - if extension_reference_key(requested) == extension_reference_key(COPILOT_REFERENCE_ALIAS) + // 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; @@ -5534,7 +5535,7 @@ mod tests { } #[test] - fn copilot_reference_is_reserved_even_when_custom_name_matches() { + fn copilot_slash_alias_is_reserved_without_redirecting_exact_custom_names() { let custom = |name: &str| ExtensionConfig::Frontend { name: name.to_string(), description: String::new(), @@ -5545,13 +5546,14 @@ 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) ), - None + Some(custom_key) ); assert_eq!( exact_custom_reference_key("computercontroller", Some(&target), &[entry]), @@ -5567,9 +5569,9 @@ mod tests { ); assert_eq!( exact_custom_reference_key( - "Biorouter Copilot", + "bioroutercopilot", Some(&target), - &[custom("Biorouter Copilot")] + &[custom("bioroutercopilot")] ), None ); From 5dd25b8c4ceee8d39f38d187d4c285de4d324812 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 18:44:10 -0700 Subject: [PATCH 15/21] fix(copilot): validate runtime against tracked pin --- .../src/computer_use/manifest.rs | 65 +++++++++++++++++-- .../tests/computer_use_contract.rs | 3 +- docs/releases/notes/v1.91.2.md | 3 + 3 files changed, 65 insertions(+), 6 deletions(-) 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/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/docs/releases/notes/v1.91.2.md b/docs/releases/notes/v1.91.2.md index d628b3555..414a8d00a 100644 --- a/docs/releases/notes/v1.91.2.md +++ b/docs/releases/notes/v1.91.2.md @@ -16,6 +16,9 @@ Copilot's controls, and addresses Windows console flashes. - 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 From 87a0d549a98c09fd655d6d390eb89de0f28691c5 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 18:48:55 -0700 Subject: [PATCH 16/21] fix(copilot): remove legacy wording from runtime notices --- crates/biorouter/src/agents/extension_manager.rs | 4 ++-- crates/biorouter/src/config/extensions.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/biorouter/src/agents/extension_manager.rs b/crates/biorouter/src/agents/extension_manager.rs index ff0fe5fb6..9f8e97d1c 100644 --- a/crates/biorouter/src/agents/extension_manager.rs +++ b/crates/biorouter/src/agents/extension_manager.rs @@ -1240,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(), @@ -1359,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(), )); } 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 { .. }) { From 9ff72d0081c9c5aecd58a3e689fa8db1e0e712b8 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 18:49:24 -0700 Subject: [PATCH 17/21] test(windows): check packaged Copilot process console path --- .github/workflows/release-artifact-smoke.yml | 13 + scripts/computer-use-package-acceptance.py | 2 + ...t-computer-use-packaged-windows-console.py | 257 ++++++++++++++++++ 3 files changed, 272 insertions(+) create mode 100644 scripts/test-computer-use-packaged-windows-console.py 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/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..e7a66adda --- /dev/null +++ b/scripts/test-computer-use-packaged-windows-console.py @@ -0,0 +1,257 @@ +#!/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 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() + 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(), + }) + 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": + raise AssertionError(f"GUI positive control did not show a 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() + threading.Thread(target=lambda: [lines.put(line) for line in process.stdout], 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 + 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}") + 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()) From 4366ea1775fee9e382b1bb4ae3fc8022aa4e341b Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 18:50:20 -0700 Subject: [PATCH 18/21] test(windows): fail fast when packaged MCP exits --- scripts/test-computer-use-packaged-windows-console.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/test-computer-use-packaged-windows-console.py b/scripts/test-computer-use-packaged-windows-console.py index e7a66adda..6b1609a5c 100644 --- a/scripts/test-computer-use-packaged-windows-console.py +++ b/scripts/test-computer-use-packaged-windows-console.py @@ -150,7 +150,11 @@ def observe(cli, wrapper): text=True, encoding="utf-8", errors="replace", creationflags=CREATE_NO_WINDOW, env=env) lines = queue.Queue() - threading.Thread(target=lambda: [lines.put(line) for line in process.stdout], daemon=True).start() + 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: @@ -167,6 +171,8 @@ def observe(cli, wrapper): 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 From cfbe18006c93bf6c1cebe3a7793acb7923eb8599 Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 18:51:29 -0700 Subject: [PATCH 19/21] fix(copilot): name helper in accessibility guidance --- .../0005-copilot-accessibility-guidance.patch | 13 +++++++++++++ vendor/computer-use/pin.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 vendor/computer-use/patches/0005-copilot-accessibility-guidance.patch 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 ba934c202..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-2", + "patch_revision": "biorouter-3", "targets": [ "darwin-arm64", "darwin-x64", From a22616cfd9b4c38a4773a5ab39771cd6afcf16ff Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 18:54:54 -0700 Subject: [PATCH 20/21] test: assert Windows PowerShell startup hide request --- ...t-computer-use-packaged-windows-console.py | 17 ++++++++++++++-- scripts/test-computer-use-windows-console.py | 20 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/scripts/test-computer-use-packaged-windows-console.py b/scripts/test-computer-use-packaged-windows-console.py index 6b1609a5c..fab4227c8 100644 --- a/scripts/test-computer-use-packaged-windows-console.py +++ b/scripts/test-computer-use-packaged-windows-console.py @@ -42,6 +42,13 @@ 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) @@ -78,6 +85,7 @@ } 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, @@ -85,6 +93,8 @@ "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) @@ -127,8 +137,9 @@ def observe(cli, wrapper): 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": - raise AssertionError(f"GUI positive control did not show a console: {positive}, exit={control.returncode}") + 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) @@ -202,6 +213,8 @@ def read_lines(): 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"} diff --git a/scripts/test-computer-use-windows-console.py b/scripts/test-computer-use-windows-console.py index 2e87e0c1a..45aa353f5 100644 --- a/scripts/test-computer-use-windows-console.py +++ b/scripts/test-computer-use-windows-console.py @@ -27,6 +27,7 @@ "os" "os/exec" "syscall" + "unsafe" ) func consoleVerdict() string { @@ -43,15 +44,26 @@ 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) @@ -147,8 +159,8 @@ def run_control(wrapper, work, real_powershell): if completed.returncode: raise AssertionError(f"unflagged control failed: {completed.stderr[:1000]!r}") result = read_probe(report, "unflagged control") - if result["verdict"] != "visible": - raise AssertionError(f"unflagged control did not show a console: {result}") + 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 @@ -191,6 +203,10 @@ def observe(patched, baseline, wrapper, real_powershell): 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, From 9df6b5a99671690c00560a0f498f5b44172301aa Mon Sep 17 00:00:00 2001 From: Wanjun Gu Date: Wed, 23 Sep 2026 20:06:39 -0700 Subject: [PATCH 21/21] fix(windows): constrain PowerShell modules for Copilot helper --- crates/biorouter-mcp/Cargo.toml | 2 +- .../biorouter-mcp/src/computer_use/runtime.rs | 106 ++++++++++++++++-- 2 files changed, 98 insertions(+), 10 deletions(-) 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/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()); }