Skip to content

fix(agent): auto-approve CLI tool permissions & improve error visibility - #321

Merged
axisrow merged 5 commits into
mainfrom
fix/agent-can-use-tool-and-visibility
Mar 31, 2026
Merged

axisrow merged 5 commits into
mainfrom
fix/agent-can-use-tool-and-visibility

Conversation

@axisrow

@axisrow axisrow commented Mar 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • can_use_tool auto-approve: Claude CLI v2.1.88 sends can_use_tool control requests for network tools (read_messages, send_message, etc.). Without a callback, the SDK raises "canUseTool callback is not provided" → tools fail silently with "Tool permission stream closed before response received". Added _auto_approve_tool callback that auto-approves all tool calls (our own PermissionGate handles permissions separately).

  • UserMessage handling: SDK yields UserMessage after tool results are sent back to Claude. Previously ignored → _last_activity not updated → idle timeout starts counting too early. Now updates _last_activity.

  • API request counter: [api:request] events were deduplicated (same label). Third API request (after tool call) was invisible. Now each API request gets a unique counter: "API Add channel statistics #1", "API Add Claude Code GitHub Workflow #2", "API Refactor project into role-based layers #3".

  • Unhandled message logging: Unknown SDK message types logged at warning level for diagnostics.

Root cause trace

Session data showed:

tool_result: "Tool permission stream closed before response received"
tool_result: "Stream closed"

Tools appeared as ✅ in TUI but actually returned errors to Claude.

Test plan

  • ruff check passes
  • 102 tests pass (pytest tests/test_agent.py tests/test_agent_errors.py -n auto)
  • test_chat_stream_options_have_can_use_tool — verifies can_use_tool is set
  • Live: python -m src.main agent chat → "Получи последние 10 сообщений из чата 1877929309"

🤖 Generated with Claude Code

axisrow and others added 2 commits March 31, 2026 16:49
…ity (#320)

- Add can_use_tool callback to auto-approve CLI permission requests.
  Without it, network tools (read_messages, send_message) fail silently
  with "Tool permission stream closed before response received".
- Handle UserMessage from SDK — update _last_activity so idle timeout
  doesn't start counting prematurely after tool results.
- Stop deduplicating [api:request] events — each API call gets a unique
  counter (API #1, #2, #3) so user can track multi-turn conversations.
- Log unhandled SDK message types at warning level.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- test_stderr_api_request_counter_not_deduped: each [api:request] gets
  unique counter (API #1, #2, #3), not deduped like other stage events
- test_user_message_does_not_break_stream: UserMessage from SDK (tool
  results sent back to Claude) doesn't break the event stream

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@axisrow

axisrow commented Mar 31, 2026

Copy link
Copy Markdown
Owner Author

@claude review. Focus on critical issues: bugs, security vulnerabilities, logical errors, data loss risks, performance problems. Do NOT nitpick style, naming conventions, minor formatting, or subjective preferences — only flag issues that could break functionality or cause real harm in production.

@claude

claude Bot commented Mar 31, 2026 •

Copy link
Copy Markdown

Claude finished @axisrow's task in 1m 39s —— View job


Code Review

  • Read changed files
  • Analyze for critical issues
  • Post review findings

Overall the changes are correct and address real bugs. No critical security vulnerabilities or data loss risks. Two minor issues worth noting:


Issue 1 — _api_request_count not reset between retry attempts (minor)

src/agent/manager.py:434 — _api_request_count is initialized once before the retry loop (line 571). On a retry, the counter continues from where it left off. So if attempt #1 emitted 3 API request statuses and then timed out, attempt #2 would show "API #4", "API #5", etc. instead of starting from "API #1".

Not a functional bug, but could confuse diagnostics.

Fix this →


Issue 2 — [api:request] early return silently drops is_error (theoretical, practically safe)

src/agent/manager.py:483-492 — The early return for [api:request] lines exits _on_stderr before the is_error surfacing block runs. This means if an [api:request] line were also classified as an error (e.g., contained [error]), it would be logged but not surfaced to the user's queue.

In practice, [api:request] lines are always [INFO] level, so is_error would always be False here. Practically a non-issue, but the code is slightly misleading — it reads as if the early return might be skipping important work.


_auto_approve_tool design — intentional, no issue

The function unconditionally returns PermissionResultAllow() regardless of tool_name/tool_input. This is correct given that:

  1. The allowed_tools list passed to ClaudeAgentOptions already restricts which tools Claude can call.
  2. When no gate is active, filter_allowed_tools() narrows the list further.
  3. can_use_tool is a secondary CLI runtime check that was crashing the tool call. Auto-approving here does not widen permissions beyond what allowed_tools already grants.

The comment in the function documents this rationale well.


UserMessage handling — correct

Updating _last_activity and doing nothing else is the right approach. UserMessage signals the SDK sent tool results back to Claude — it's a heartbeat, not a displayable event.


Summary: No bugs that could break functionality in production, no security issues, no data loss risks. The two issues above are cosmetic/theoretical. The core fixes (can_use_tool callback, UserMessage heartbeat, deduplicated API counter) are sound.

@axisrow

axisrow commented Mar 31, 2026

Copy link
Copy Markdown
Owner Author

Re: Issue 2 ([api:request] early return) — SKIP. [api:request] lines are always [INFO] level status events from the SDK, never error lines. The early return is intentional (unique counter, must not be deduped). The theoretical risk of dropping a real error here is negligible.

_api_request_count was not reset between retry attempts, causing
"API #4" instead of "API #1" on retries. Reset to 0 at the start
of each attempt alongside other per-attempt state.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@axisrow

axisrow commented Mar 31, 2026

Copy link
Copy Markdown
Owner Author

@claude review. Focus on critical issues: bugs, security vulnerabilities, logical errors, data loss risks, performance problems. Do NOT nitpick style, naming conventions, minor formatting, or subjective preferences — only flag issues that could break functionality or cause real harm in production.

@claude

claude Bot commented Mar 31, 2026 •

Copy link
Copy Markdown

Claude finished @axisrow's task in 1m 35s —— View job


Code Review

  • Read changed files
  • Analyze for critical issues
  • Post review findings

The previous review found two issues. Here's the current status:

Issue 1 — _api_request_count not reset on retry: ✅ Fixed in eca9542. _api_request_count[0] = 0 is now at line 581, inside the for attempt in range(2) loop, so each retry starts the counter fresh.

Issue 2 — [api:request] early return drops is_error block: ✅ Accepted as SKIP by author — [api:request] lines are always [INFO] level, is_error would never be True here.


No critical issues found in the current diff

All four changes are correct:

  • _auto_approve_tool (manager.py:354): correctly auto-approves CLI can_use_tool requests. Safe because allowed_tools passed to ClaudeAgentOptions is the real permission gate — this callback is only a secondary CLI runtime check that was crashing tool execution.

  • _api_request_count counter (manager.py:481–489): early-return path is correct. Each [api:request] event gets a unique numbered label, bypassing the deduplication guard (_last_emitted). Reset at line 581 per retry.

  • UserMessage handling (manager.py:726): updating _last_activity[0] is the right behavior — it's a heartbeat signaling tool results were forwarded to Claude, not a displayable event.

  • Unhandled message logging (manager.py:751): warning-level log is appropriate for diagnostics.

Tests

Three new tests cover all three behavioral changes. Test logic is sound:

  • test_chat_stream_options_have_can_use_tool — asserts can_use_tool is set on options
  • test_stderr_api_request_counter_not_deduped — verifies 3 unique API #N status labels
  • test_user_message_does_not_break_stream — verifies stream completes with text and done when UserMessage is interspersed

The filter "text" in p and "type" not in p in the last test is correct — text-delta payloads in this codebase are emitted as {"text": "..."} (no type key, see manager.py:690), unlike status/tool events.


No blocking issues. Ready to merge.

axisrow and others added 2 commits March 31, 2026 17:07
…ut error

When API rate limit status is "rejected", emit ⚠️ warning event to user
instead of a silent status line. Include last rate limit info in idle
timeout error message so user understands WHY the agent hung.

Before: "Стрим Claude замолчал на 90с. Возможно, соединение потеряно."
After:  "Стрим Claude замолчал на 90с. Последний rate limit: rejected."

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
_last_rate_limit stored "Rate limit: status, NN%" but timeout message
prepended another "Последний rate limit:" → double prefix.
Now stores just "status, NN%" for clean output.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@axisrow
axisrow merged commit aa9442a into main Mar 31, 2026
3 of 5 checks passed
@axisrow
axisrow deleted the fix/agent-can-use-tool-and-visibility branch March 31, 2026 09:19
@axisrow axisrow added the bug Something isn't working label Jun 8, 2026
axisrow added a commit that referenced this pull request Sep 29, 2026
…ity (#321)

* fix(agent): auto-approve CLI tool permissions & improve error visibility (#320)

- Add can_use_tool callback to auto-approve CLI permission requests.
  Without it, network tools (read_messages, send_message) fail silently
  with "Tool permission stream closed before response received".
- Handle UserMessage from SDK — update _last_activity so idle timeout
  doesn't start counting prematurely after tool results.
- Stop deduplicating [api:request] events — each API call gets a unique
  counter (API #1, #2, #3) so user can track multi-turn conversations.
- Log unhandled SDK message types at warning level.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* test(agent): cover API request counter and UserMessage handling

- test_stderr_api_request_counter_not_deduped: each [api:request] gets
  unique counter (API #1, #2, #3), not deduped like other stage events
- test_user_message_does_not_break_stream: UserMessage from SDK (tool
  results sent back to Claude) doesn't break the event stream

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(agent): reset API request counter on retry attempt

_api_request_count was not reset between retry attempts, causing
"API #4" instead of "API #1" on retries. Reset to 0 at the start
of each attempt alongside other per-attempt state.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(agent): surface rate limit rejected as warning & include in timeout error

When API rate limit status is "rejected", emit ⚠️ warning event to user
instead of a silent status line. Include last rate limit info in idle
timeout error message so user understands WHY the agent hung.

Before: "Стрим Claude замолчал на 90с. Возможно, соединение потеряно."
After:  "Стрим Claude замолчал на 90с. Последний rate limit: rejected."

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(agent): remove duplicate "Rate limit:" prefix in timeout error

_last_rate_limit stored "Rate limit: status, NN%" but timeout message
prepended another "Последний rate limit:" → double prefix.
Now stores just "status, NN%" for clean output.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant