mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-27 00:44:57 +08:00
fix: #159 — run_turn_loop no longer hardcodes empty denied_tools; permission denials now parity-match bootstrap_session
#159: multi-turn sessions had a silent security asymmetry: denied_tools were always empty in run_turn_loop, even though bootstrap_session inferred them from the routed matches. Result: any tool gated as 'destructive' (bash-family commands, rm, etc) would silently appear unblocked across all turns in multi-turn mode, giving a false 'clean' permission picture to any claw consuming TurnResult.permission_denials. Fix: compute denied_tools once at loop start via _infer_permission_denials, then pass the same denials to every submit_message call (both timeout and legacy unbounded paths). This mirrors the existing bootstrap_session pattern. Acceptance: run_turn_loop('run bash ls').permission_denials now matches what bootstrap_session returns — both infer the same denials from the routed matches. Multi-turn security posture is symmetric. Tests (tests/test_run_turn_loop_permissions.py, 2 tests): - test_turn_loop_surfaces_permission_denials_like_bootstrap: Symmetry check confirming both paths infer identical denials for destructive tools - test_turn_loop_with_continuation_preserves_denials: Denials inferred at loop start are passed consistently to all turns; captured via mock and verified non-empty Full suite: 82/82 passing, zero regression. Closes ROADMAP #159.
This commit is contained in:
@@ -204,6 +204,9 @@ class PortRuntime:
|
||||
matches = self.route_prompt(prompt, limit=limit)
|
||||
command_names = tuple(match.name for match in matches if match.kind == 'command')
|
||||
tool_names = tuple(match.name for match in matches if match.kind == 'tool')
|
||||
# #159: infer permission denials from the routed matches, not hardcoded empty tuple.
|
||||
# Multi-turn sessions must have the same security posture as bootstrap_session.
|
||||
denied_tools = tuple(self._infer_permission_denials(matches))
|
||||
results: list[TurnResult] = []
|
||||
deadline = time.monotonic() + timeout_seconds if timeout_seconds is not None else None
|
||||
|
||||
@@ -225,7 +228,8 @@ class PortRuntime:
|
||||
|
||||
if deadline is None:
|
||||
# Legacy path: unbounded call, preserves existing behaviour exactly.
|
||||
result = engine.submit_message(turn_prompt, command_names, tool_names, ())
|
||||
# #159: pass inferred denied_tools (no longer hardcoded empty tuple)
|
||||
result = engine.submit_message(turn_prompt, command_names, tool_names, denied_tools)
|
||||
else:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
@@ -233,7 +237,7 @@ class PortRuntime:
|
||||
break
|
||||
assert executor is not None
|
||||
future = executor.submit(
|
||||
engine.submit_message, turn_prompt, command_names, tool_names, ()
|
||||
engine.submit_message, turn_prompt, command_names, tool_names, denied_tools
|
||||
)
|
||||
try:
|
||||
result = future.result(timeout=remaining)
|
||||
|
||||
Reference in New Issue
Block a user