fix(cua-driver): hide policy-disabled MCP tools - #3132
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the Cua Driver MCP tools/list response to reflect permission-policy enforcement, aiming to hide tools that are denied by policy while keeping allowed tools visible and keeping invocation fail-closed.
Changes:
- Added a regression test asserting that policy-denied tools are not advertised via
tools/listand remain denied at invocation time. - Updated
ToolRegistry::tools_listto filter the advertised tool roster via the policy authorization path.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| libs/cua-driver/rust/crates/cua-driver/tests/policy_tools_list_test.rs | Adds an integration test covering tools/list behavior under an explicit allow/deny policy. |
| libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs | Filters the tools/list roster through policy evaluation before returning tool entries. |
Suppressed comments (1)
libs/cua-driver/rust/crates/cua-driver/tests/policy_tools_list_test.rs:39
- After adding an
allow.rulesentry, assert that the corresponding tool name is present in thetools/listroster (in addition to theallow.toolsanddeny.toolsassertions). This makes the test catch the common failure mode wheretools/listis evaluated with empty args and therefore denies all rule-constrained tools.
assert!(
names.contains("get_config"),
"allowed tool must remain listed"
);
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+813
to
+817
| let empty_args = Value::Object(serde_json::Map::new()); | ||
| let list: Vec<Value> = self | ||
| .order | ||
| .iter() | ||
| .filter_map(|n| self.tools.get(n)) | ||
| .map(|t| t.def().to_list_entry()) | ||
| .filter(|name| crate::policy::authorize_tool_call(name, &empty_args).is_ok()) |
Comment on lines
+14
to
+17
| std::fs::write( | ||
| &policy_path, | ||
| "allow:\n tools: [get_config]\ndeny:\n tools: [list_apps]\n", | ||
| ) |
Replace the tools_list filter that called authorize_tool_call with empty args with a new is_tool_listable function. The old approach incorrectly hid tools that are conditionally allowed via allow.rules, because arg constraints can never be satisfied by an empty argument object. is_tool_listable checks only whether a tool has any potential allow path: for YAML policies it returns true when the tool is not explicitly denied and appears in either allow.tools or allow.rules; for Rego it falls back to evaluating with empty args as before. Tools that pass this check remain in the roster so callers can attempt a constrained invocation. Also expand the policy_tools_list_test to include an allow.rules entry and assert that the rule-constrained tool still appears in tools/list. Co-authored-by: r33drichards <57335981+r33drichards@users.noreply.github.com>
Co-authored-by: r33drichards <57335981+r33drichards@users.noreply.github.com>
r33drichards
added a commit
that referenced
this pull request
Aug 13, 2026
Two changes merged to main after this page was written and left parts of it describing behaviour that no longer exists. #3133 added a public Sandbox.exposed_ports property that falls back to the saved sandbox state, so the page no longer needs to reach into _runtime_info, and the callout warning that a reconnecting caller cannot recover the port is simply wrong now. Rewrite it around the property, keeping the local/Fleet split the property's own docstring draws: exposed_ports locally, tunnel.forward() on Fleet, where it is empty. #3132 filters the MCP tools/list roster through the capability policy, so denied tools are no longer advertised. The page presented "list_tools() advertises everything, the policy refuses at call time" as the rule; that holds only for driver builds predating the fix. Attribute the 55-tool observation to the image used here, say the listing may already be filtered, and tell readers not to assume the listing and the callable set match in either direction. The client-side trim is unaffected and still worth doing, so its rationale is restated without leaning on the count.
r33drichards
added a commit
that referenced
this pull request
Aug 13, 2026
Two changes merged to main after this page was written and left parts of it describing behaviour that no longer exists. #3133 added a public Sandbox.exposed_ports property that falls back to the saved sandbox state, so the page no longer needs to reach into _runtime_info, and the callout warning that a reconnecting caller cannot recover the port is simply wrong now. Rewrite it around the property, keeping the local/Fleet split the property's own docstring draws: exposed_ports locally, tunnel.forward() on Fleet, where it is empty. #3132 filters the MCP tools/list roster through the capability policy, so denied tools are no longer advertised. The page presented "list_tools() advertises everything, the policy refuses at call time" as the rule; that holds only for driver builds predating the fix. Attribute the 55-tool observation to the image used here, say the listing may already be filtered, and tell readers not to assume the listing and the callable set match in either direction. The client-side trim is unaffected and still worth doing, so its rationale is restated without leaning on the count.
r33drichards
added a commit
that referenced
this pull request
Aug 13, 2026
#3131) * docs(sandbox): add a how-to for running Minecraft in a Windows sandbox Walks through booting Image.windows() on the local bare-metal QEMU runtime, installing Minecraft Java Edition inside it, and handing the launcher to a computer-use agent. The page documents the four traps that make this fail otherwise: - the bare-metal netdev is restrict=on, so the guest has no internet until a second NIC is added, and that NIC needs its own subnet or both user-mode networks offer the guest the same address - the default qemu64 CPU model is too thin for a software OpenGL driver: the game creates its window and then dies during resource loading with no Java exception, which -cpu host fixes - MSVC builds of both Prism Launcher and Mesa3D need the VC++ redistributable that Windows Server 2022 does not ship, so the MinGW builds are the ones that work - the launcher downloads its own Java, so the Mesa DLLs have to land next to that javaw.exe rather than a separately installed JRE Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(sandbox): drop the obsolete shared-efivars troubleshooting row EFI variables stopped being shared in #3128: the bare-metal QEMU runtime now derives one pflash file per VM from its disk name (Path(disk_path).with_suffix(".efivars.fd"), runtime/qemu.py:345), and tests/test_docs_regressions.py asserts the old shared path is gone. There is no ~/.cua/cua-sandbox/images/sessions/efivars.fd to delete anymore, so the row pointed readers at a file that does not exist. * docs(sandbox): drive Minecraft through cua-driver's MCP server Rewrites the agent half of the guide around the MCP server the sandbox already runs, rather than ComputerAgent: Image.windows().expose(3000) publishes cua-driver's endpoint on guest :3000, a FastMCP client lists its tools, and a small loop hands them to a model as ordinary function tools. The same loop runs against a local sandbox and against Fleet; only the transport differs. Presenting MCP tools as ordinary function tools also sidesteps endpoints that refuse provider-native computer-use tool types. Documents three things about cua-driver that are otherwise a surprise: a YAML policy refuses tools that list_tools() still advertises, clicks are addressed to a pid rather than the screen, and there is no wait tool so the loop waits by re-screenshotting. Also adds prerequisites (cua-sandbox 0.3.1+, and that -cpu host needs KVM or HVF, so an x86_64 guest on Apple Silicon cannot use the local path), a delivery snippet for the Mesa script, and replaces the stale Fleet callout now that Windows boots there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(sandbox): use the public tunnel API and make the sample match the run Replaces the hand-assembled Fleet service URL with sb.tunnel.forward(3000), which returns exactly that URL and is already documented in tunneling.mdx. The local path keeps reading the port off the object Sandbox.create returns, because tunnel.forward() is not implemented for that transport and the port is not recoverable after a reconnect — now stated outright rather than left for a reader to discover. The agent sample now does what the prose said it did: streams and rebuilds with stream_chunk_builder, folds the system prompt into the first user turn, prunes old screenshots, and trims the 55-tool listing to the handful the task needs. Also explains why -cpu host is absent on Fleet, attributes the token lifetime to what was observed rather than asserting it, and drops an unused import. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(sandbox): drop the /docs prefix from the tunneling cross-link Internal doc links in docs/content are written relative to the docs root without a /docs segment — every other absolute link in the tree does it this way (for example ](/how-to-guides/sandbox/images)), and there were zero occurrences of ](/docs/ before this one. The prefixed form resolves to /docs/how-to-guides/... at render time and 404s. * docs(sandbox): record that Minecraft does not yet run on Fleet The Fleet path is verified as far as the sandbox and the agent: it boots, .expose(3000) publishes cua-driver, the MCP client lists the same tools, the setup script installs Prism and Mesa, the Microsoft sign-in completes, and the agent loop drives the desktop. The game itself exits during resource loading every time, with no Java exception, no hs_err and nothing in the event log. Neither known cause explains it: the Fleet CPU reports full AVX-512, so the qemu64 instruction theory does not apply, and forcing the heap from Prism's auto-sized -Xmx2717m down to -Xmx1024m on the 4 GB sandbox changed nothing. Unlike the local path there are no QEMU arguments to adjust. Says so plainly rather than implying the game runs there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(sandbox): pin down the Fleet crash with an exit code and a second version Adds the evidence that narrows it: the game dies with exitcode -2147024809, and it is not version-specific — 1.20.1 on Java 17 with LWJGL 3 and 1.12.2 on Java 8 with LWJGL 2 stop at the same point. That exit code is the same one the local qemu64 CPU model produced, but the Fleet CPU reports AVX, AVX2 and AVX-512 present, so the local explanation does not carry over. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(sandbox): rule out machine size for the Fleet crash Records the last elimination so nobody repeats the experiment: the same local session disk, rebooted with -m 4096 -smp 4 to match the Fleet sandbox exactly, runs Minecraft fine. Prism warns about low free memory and the game launches anyway. So the Fleet failure is not resources. Five things are now ruled out -- CPU feature flags, heap size, Minecraft version, Java/LWJGL generation, and machine size -- with the same stopping point and the same exitcode -2147024809 throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(sandbox): restore the tunneling cross-link path The /docs prefix came back in 2a4c385 when the surrounding Fleet paragraph was rewritten, undoing 5f19e66. Internal links in docs/content omit the /docs segment — every other absolute link in the tree is written ](/how-to-guides/...) and this is the only ](/docs/ in it — so the prefixed form 404s at render time. * docs(sandbox): stop sending Apple Silicon readers to Fleet for the game The prerequisites and the -cpu host troubleshooting row both told readers without KVM or HVF to use the Fleet path instead. Since 93c1cdd the guide also records that Minecraft does not run on Fleet, so those two lines sent the one reader who cannot use the local path to the one path where the game is known not to work. Both now say what Fleet does get you — the sandbox and the agent loop — and point at the Fleet section for the rest. * docs(sandbox): Minecraft runs on Fleet with GALLIUM_DRIVER=softpipe The Fleet crash was Mesa's llvmpipe renderer. Switching to softpipe fixes it, and an agent drove the same loop from launcher to standing in a new world on Fleet in 51 steps. The section documents the workaround instead of the failure, and the Apple Silicon note now sends readers to Fleet for the game rather than away from it. Keeps the eliminations as the reason softpipe is needed rather than a guess: not the Minecraft version, not the Java/LWJGL generation, not the heap, not the machine size, and not simply wide-vector codegen -- narrowing llvmpipe with LP_NATIVE_VECTOR_WIDTH=128 did not help. Notes that softpipe has no JIT and is markedly slower, and that the variable must be set in the process that launches the launcher, since a machine-level variable does not reach an already-running process. Also records that Fleet sizing is account-dependent: cpu/memory_mb route through a gated path that returned 403 until a card requirement was lifted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(sandbox): make the resource-loading crash findable for Fleet readers The troubleshooting table had one row for "game exits during resource loading", attributing it to the qemu64 CPU model and prescribing -cpu host. Since b8ab92f the same symptom has a second, unrelated cause on Fleet — Mesa's llvmpipe renderer — and -cpu host is not available there at all, because Fleet takes no runtime= and no QEMU arguments. A Fleet reader looking the symptom up was sent to a fix they cannot apply, while the one that works had no row. Scope the existing row to local, add a Fleet row pointing at GALLIUM_DRIVER=softpipe, and note in the Fleet callout that setup.ps1 has already started Prism without the variable, so the launcher has to be restarted for it to take effect. * docs(sandbox): catch up with the exposed-ports and tool-listing fixes Two changes merged to main after this page was written and left parts of it describing behaviour that no longer exists. #3133 added a public Sandbox.exposed_ports property that falls back to the saved sandbox state, so the page no longer needs to reach into _runtime_info, and the callout warning that a reconnecting caller cannot recover the port is simply wrong now. Rewrite it around the property, keeping the local/Fleet split the property's own docstring draws: exposed_ports locally, tunnel.forward() on Fleet, where it is empty. #3132 filters the MCP tools/list roster through the capability policy, so denied tools are no longer advertised. The page presented "list_tools() advertises everything, the policy refuses at call time" as the rule; that holds only for driver builds predating the fix. Attribute the 55-tool observation to the image used here, say the listing may already be filtered, and tell readers not to assume the listing and the callable set match in either direction. The client-side trim is unaffected and still worth doing, so its rationale is restated without leaning on the count. * docs(sandbox): state the tool-listing behaviour as fact, not a hedge Every published cua-driver release advertises the full tool surface and refuses out-of-policy calls only at call time: tags 0.19.2 (2026-08-07), 0.19.3 (2026-08-10) and the 0.19.4 nightly (2026-08-12) all predate the change that filters the listing, which merged on 2026-08-13. So for any reader today this is simply how it behaves, and the previous "depends on your image / do not assume either direction" wording cost them a concrete expectation for no gain. State it plainly, keep the probe-your-own-image advice for the allow list itself, and note that later drivers make the listing and the callable set agree. The troubleshooting row is scoped the same way. * docs(sandbox): raise the version floor to 0.3.2 for sb.exposed_ports I moved the page onto the public Sandbox.exposed_ports property in b2bebc1 without revisiting the prerequisite, which still said 0.3.1. The property arrived with #3133, merged 2026-08-13T17:49Z — after cua-sandbox 0.3.1 went to PyPI at 04:07 that morning and before 0.3.2 at 17:54. So a reader installing the version the page asked for gets AttributeError on the page's own first example. Floor is 0.3.2. The bullet now names what each version is needed for, so the next person to use a newer API has the mapping in front of them. --------- Co-authored-by: Robert Wendt <robert@trycua.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
tools/listroster through the same canonical capability-policy decision used before invocation.Validation
cargo test -p cua-driver-core --lockedcargo test -p cua-driver --test protocol_schema_test --lockedcargo test -p cua-driver --test permission_policy_startup_test --lockedcargo clippy -p cua-driver --test policy_tools_list_test --locked --no-depscargo fmt --all -- --checkgit diff --checkNo release or deployment performed.