fix(tools): empty execute_code capability set denies all sandbox tools - #86148
Conversation
fix(tools): empty execute_code capability set denies all sandbox tools — solid security fix with good tri-state tests. Observations:
|
89653a5 to
2535466
Compare
|
Extracted |
|
CI slice 1 failed on Pinned |
|
Code review — changes requested The shared 1. Please test the actual RPC authorization boundary. The current regressions only prove that a named Please add local UDS and remote file-RPC tests that call 2. Carry the tri-state semantics through The resolver itself looks correct. These are closure and integration gaps around the security boundary, not objections to the central implementation. |
d4ec8e2 to
8dfab73
Compare
8dfab73 to
8a90347
Compare
|
Rebased this existing PR onto current main after CI slice 7 reported the unrelated |
|
The arm64 Docker job failed before the build because GitHub returned HTTP 429 while downloading the pinned setup-uv action. No source/build error was reached; rerunning failed jobs is restricted to repository administrators. |
8a90347 to
31d7f36
Compare
|
Rebased once more onto the latest main after the arm64 job hit the GitHub action-download rate limit. The focused loop/plugin/code-execution verification remains 66/66; updated head is |
31d7f36 to
1d68f21
Compare
|
Maintenance update on the existing PR:
The existing branch was updated in place. No new PR was opened. |
32cbc9c to
f6f90cd
Compare
|
Maintenance update: rebased onto exact current |
andrexibiza
left a comment
There was a problem hiding this comment.
Reviewed exact head f6f90cd9d15ad0b94299dced678feb39ade0ac17 against live main@68518c1f9bca11d9f5dbdf59ecf7e024cce057ba, including the complete three-file diff, both sandbox RPC transports, the public dispatch handoff in model_tools.py, the #84271 acceptance contract, existing review thread, exact-head CI, and the current execute-code PR collision set.
Verdict: clear on code at this head. I do not find a remaining blocker in SECURITY-CLASS-faf9d60580300e16.
The important thing this revision gets right is that the capability decision now has one meaning all the way to the mutation boundary:
_resolve_sandbox_tools()makes the distinction structural rather than truthiness-based:Noneis the explicitly documented legacy default, while[]and a non-overlapping grant remain deny-all.- Both
execute_code()and_execute_remote()consume that same resolver, so local and remote execution no longer carry two independently drifting interpretations. - This is not only a stub-generation fix.
_rpc_server_loopand_rpc_poll_loopboth enforcetool_name not in allowed_toolsat dispatch, and the new tests deliberately invoke the generated module's private_call()against the real local/file RPC authorization loops. Empty and non-overlapping manifests reject bothterminalandwrite_file, record zero underlying dispatches, and the partial-grant cases prove exact intersection rather than blanket denial. _sandbox_failure_hint()now consumes the same tri-state contract, so deny-all no longer produces an operator-facing lie that every sandbox tool is importable.- The public handoff in
model_tools.handle_function_call()preserves an explicit empty list withenabled_tools if enabled_tools is not None else _last_resolved_tool_names; the repaired sandbox therefore receives the caller's actual empty grant instead of re-broadening it before this PR's resolver sees it.
That closes the two concrete gaps called out earlier on this thread: direct RPC authorization is now tested on both transports, and the diagnostic projection no longer widens an empty capability set. The shared-helper observation from the earlier automated review is also fully absorbed rather than copied twice. Nice repair work, @Christopher-Schulze. 🚀
Exact-object evidence
The exact head is green across the hosted workflows:
- CI
33004900173— success, includingcheck-attribution, common-ancestor, OSV, and supply-chain jobs. - Docker
33004897830— success. - Nix
33004898028— success.
The branch is currently 6 commits ahead / 2 behind live main with merge base f0187332d13679e2ace74d1f0eb399eb91a90571. I checked both main-only commits (01e9b9ab and 68518c1f): they touch only Desktop use-background-sync.ts + its test, so the landing drift is path-disjoint from this PR's tools/code_execution_tool.py, tests/tools/test_code_execution.py, and tests/agent/test_plugin_prompt_sections.py. I do not see a semantic reason to churn this security patch solely for those two Desktop ticks; normal landing-edge CI remains the release authority.
Interlocks / merge order
This is the small security invariant I would land before the larger execute-code changes currently in flight. It owns nested sandbox capability admission, not session provenance, output formatting, redaction, kernel lifetime, or container realization:
- #52000 (also @Christopher-Schulze) is complementary nested-RPC
session_idprovenance and directly touchescode_execution_tool.py/test_code_execution.py. When restacked, it should preserve this exact resolved capability set while threading session identity through the same loops. - #94875 is complementary stable programmatic
read_filesemantics and also crosses these RPC loops. Its dispatcher-routing work must retain this allow-list before applying the read-mode specialization. - #95515 is complementary output confidentiality. It changes execute-code redaction after execution; it must not disturb the admission manifest established here.
- #94647 is the larger persistent-kernel feature. Because a kernel outlives one call, this deny-all/partial-grant invariant is even more important there: any reused kernel/RPC server must be keyed or refreshed so it cannot inherit a broader capability set from an earlier cell.
- #90050 is complementary container realization/config projection; no capability ownership transfer belongs there.
- #65592 is complementary approval/AST policy. Approval decides whether Python execution may start; this PR independently decides which Hermes tools that execution may invoke. Neither should stand in for the other.
So I would treat #86148 as the capability-admission base and make later shared-file PRs semantically re-read against it rather than resolving conflicts textually.
The unrelated test_plugin_prompt_sections.py edits are test-harness isolation only: they pin workspace context so detached CI merge checkouts do not make the cache assertion nondeterministic; they do not change runtime behavior.
This is a good example of closing the whole security shape instead of just changing the conditional: grant construction, generated surface, local enforcement, remote enforcement, diagnostics, and exact negative witnesses now agree. 🚀
f6f90cd to
5fb6edf
Compare
|
Maintenance update: rebased onto current |
enabled_tools=[] was conflated with enabled_tools=None via truthiness, so an explicit empty grant broadened to every SANDBOX_ALLOWED_TOOLS stub instead of denying all. Preserve tri-state semantics: None keeps the legacy default (every sandbox tool), an explicit list (possibly empty) uses the exact intersection, so [] and a non-overlapping list both deny all. Fixes NousResearch#84271
Local UDS and remote file-RPC now resolve enabled_tools through one helper so the deny-all / legacy-default contract cannot drift.
CI merge checkouts are detached HEAD; a second status probe can drop the branch line and fail first==rebuilt. Pin context cwd so this test only covers plugin-section caching.
Address the review follow-up by testing local and remote RPC authorization directly, keeping sandbox failure hints tri-state-aware, and proving partial grants cannot dispatch unapproved tools.
Add narrow casts and assertions around dynamically executed RPC stubs so the new authorization tests introduce no Ty diagnostics.
Keep fresh-process prompt bytes stable when GitHub merge checkouts are detached and workspace metadata changes between builds.
5fb6edf to
c41e872
Compare
What does this PR do?
Fixes a security-hardening gap where
execute_codeconflated an explicit empty sandbox grant with an omitted grant. An explicitenabled_tools=[]or a non-overlapping grant previously broadened to every sandbox tool in both the local UDS and remote file-RPC paths.The resolver now preserves the tri-state contract:
enabled_tools=None→ legacy default: all sandbox tools.enabled_tools=[]→ deny-all.SANDBOX_ALLOWED_TOOLS.Related Issue
Part of the SECURITY-AUDIT-42 hardening campaign (EPIC #82591), class
SECURITY-CLASS-faf9d60580300e16.Fixes #84271
Type of Change
Changes Made
tools/code_execution_tool.pynow shares_resolve_sandbox_tools()across local UDS and remote file-RPC execution._call()boundary tests for empty, non-overlapping, and partial grants on both transports; denied requests never reach the underlying handler._sandbox_failure_hint()preserve the same tri-state semantics and reportnoneinstead of advertising unavailable tools.How to Test
scripts/run_tests.sh tests/tools/test_code_execution.py tests/agent/test_plugin_prompt_sections.py→ 58 passed.scripts/check.sh --project hermes-agent --worktree worktrees/hermes-agent/84271→ all blocking gates passed (uv lock --check, Ruff, and 58 changed-file tests).origin/main, direct local UDS and remote file-RPC requests withenabled_tools=[]reached their handlers; on this branch, the corresponding tests reject them and record no handler dispatch.Final verification: head
5fb6edfc786c41efedfa4d73dc5cc817ec1f4b07, rebased ontoorigin/main3aee290899e478c5fdfb6a241ef62758a49829b3; the changed-file suite reports 58 passed andscripts/check.sh --project hermes-agent --worktree worktrees/hermes-agent/84271passes all blocking gates.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/tools/test_code_execution.py -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AScreenshots / Logs
Not applicable; this is a Python security fix with automated local and remote RPC regression coverage.