fix(permission): scan the tool title off the event loop too - #8278
Closed
bolichen97 wants to merge 1 commit into
Closed
fix(permission): scan the tool title off the event loop too#8278bolichen97 wants to merge 1 commit into
bolichen97 wants to merge 1 commit into
Conversation
The title tier of _resolve_permission -- is_sensitive_path, is_sensitive_bash_command and is_denied on event.title -- still ran inline on the event loop after kirodotdev#7941 moved the tool_input scan onto a worker. For a shell tool the title IS the command, and a ~9 KB command emitted by an hourly cron held the loop past the 25 s stall watchdog (crash dump: security.py inside is_sensitive_bash_command, called from _resolve_permission at the title check). The offload that existed did not cover the crash path. Title and tool_input now go through one asyncio.to_thread hop, title first, so a request denied on its title keeps the title-tier reason and the always_deny mechanism, and a payload-string denial keeps always_deny_input. The empty-title refusal stays on the loop (no scan). The comments claimed CPython's re holds the GIL for a whole match, which would have made the hop pointless within one scan. Measured on 3.10, 3.11 and 3.12: a 16-23 s search on a worker thread left the main thread's 20 ms tick gaps at 20 ms, so re does release the GIL while matching, the loop keeps ticking, and a caller's asyncio.wait_for can cancel the await. The size ceiling stays as a bound on the worker. hooks.on_tool_call (HOOK_BASED policy and the channel dispatchers that call it synchronously) still runs inline; its liveness bound is the gate's own linear cost and ceiling, stated in the spec.
Collaborator
Author
|
Superseded by #8282, which carries this change together with its two companions as one PR. |
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.
Problem / Motivation
After #7941 moved the tool_input scan in
_resolve_permissiononto a worker thread, the title tier —is_sensitive_path,is_sensitive_bash_commandandis_deniedonevent.title— still ran inline on the event loop. For a shell tool the title is the command. A user's hourly cron emitted a ~9 KB command; the crash dump shows the main thread insecurity.pyinsideis_sensitive_bash_command, called from_resolve_permissionat the title check, for the full 25 s watchdog budget. The offload that existed did not cover the crash path.Why it matters
stream_and_collect→_resolve_permissionis the funnel for every streamed permission request on cron, Slack, the dashboard side panel and workflows. A long title there stalled the loop for as long as the scan took; #8277 makes the scan itself linear and bounded, this PR takes it off the loop so the loop is live even while it runs, and a caller'sasyncio.wait_forcan cancel the await.What changed (motivation → approach → change)
_title_denial(title, denied_regexes)— the three title checks, pure and synchronous, returning the exact reason strings the inline code produced._resolve_permissionruns the title tier and the tool_input tier in oneasyncio.to_threadhop, title first, so a request denied on its title keeps the title-tier reason and thealways_denymechanism and a payload-string denial keepsalways_deny_input. The empty-title refusal stays on the loop (nothing to scan)._regex_deny_mechanismstill runs on the loop afterwards, because it consults the HookManager.reholds the GIL for a whole match call, which would have made a worker hop pointless within one scan. Measured on 3.10, 3.11 and 3.12: a 16–23 sre.searchon a worker thread left the main thread's 20 ms tick gaps at 20 ms.rereleases the GIL while matching, so the hop keeps the loop live within a scan as well as between scans. The comments now say so; the size ceiling stays as a bound on the worker's own wall clock.hooks.on_tool_call(HOOK_BASED policy, and the channel dispatchers that call it synchronously) still runs inline; its liveness bound is the gate's own linear cost and ceiling (fix(security): make the shell sensitive-path gate linear and bounded #8277), stated in the spec.Spec:
docs/system-specs/modules/security.md.Tests
test/test_llm_helpers_tool_input_offload.py::TestTitleTierOffLoop:always_denyThe existing equivalence, off-loop and liveness tests pass unchanged (the
_resolvehelper gained atitleparameter).Manual verification
GIL measurement script (worker-thread
re.searchvs main-thread tick gaps) run on 3.10.20, 3.11.15, 3.12.13/3.12.14. N/A beyond that — unit coverage exercises the real_resolve_permission.Related Issues
Companion to #8277 (linear gate) and #8279 (cron loop-stall attribution + breaker + doctor). Independently mergeable: without #8277 the scan is slower but the loop stays live; with it the worker returns in tens of milliseconds.
Pattern harvest
Rule candidate: review-prompt
Pattern: an offload that moves a loop over N inputs to a worker while the same predicates still run inline on the request's primary field (the title) — check every call site of the offloaded predicates on the same path, not only the loop.
Checklist