Skip to content

feat: add permission request UI for sensitive tool operations#1200

Open
Gkrumbach07 wants to merge 9 commits intomainfrom
feat/permission-request-ui
Open

feat: add permission request UI for sensitive tool operations#1200
Gkrumbach07 wants to merge 9 commits intomainfrom
feat/permission-request-ui

Conversation

@Gkrumbach07
Copy link
Copy Markdown
Contributor

@Gkrumbach07 Gkrumbach07 commented Apr 3, 2026

Summary

  • Implements can_use_tool callback in the Claude SDK adapter to intercept sensitive tool operations (e.g., editing .mcp.json)
  • Emits synthetic PermissionRequest tool calls through the AG-UI event stream, reusing the existing halt-interrupt-resume pattern from AskUserQuestion
  • Adds a new PermissionRequestMessage frontend component with Allow/Deny buttons that surfaces permission requests in the session chat UI
  • Tracks approved operations per-session so retries succeed automatically without re-prompting

Test plan

  • Frontend build passes (npm run build — 0 errors, 0 warnings)
  • Frontend type check passes (tsc --noEmit)
  • All 615 frontend unit tests pass (npx vitest run)
  • Manual test: create a session, ask Claude to edit a sensitive file, verify permission UI appears with Allow/Deny buttons
  • Manual test: click Allow, verify Claude retries and the edit succeeds
  • Manual test: click Deny, verify Claude receives the denial and adjusts its approach

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Permission request UI: users can Approve/Deny tool requests (description, timestamp, optional file/command) with pending/approved/denied states and submission controls.
    • Permission requests are integrated into message rendering and respected across runs so prior approvals persist.
  • Refactor

    • Unified detection for whether a tool produced a result for consistent gating and disabling of controls.
    • Standardized tool-name handling and expanded human-in-the-loop detection to include permission requests.

When Claude Code SDK needs user approval for sensitive operations (e.g.
editing .mcp.json), the can_use_tool callback now emits a synthetic
PermissionRequest tool call that halts the stream and surfaces an
interactive Allow/Deny UI in the frontend. Approved operations are
tracked per-session so retries succeed automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Gkrumbach07 Gkrumbach07 added the ambient-code:managed PR managed by AI automation label Apr 3, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

Adds a permission-request human-in-the-loop flow: new PermissionRequest tool input and frontend UI, shared result-presence helper, and adapter/runtime plumbing that emits synthetic permission events, awaits user approval, and resumes or denies tool execution accordingly.

Changes

Cohort / File(s) Summary
Frontend Permission UI
components/frontend/src/components/session/permission-request.tsx, components/frontend/src/components/session/ask-user-question.tsx, components/frontend/src/components/ui/stream-message.tsx
Added PermissionRequestMessage component; replaced local hasResult in ask-user-question.tsx with shared hasToolResult usage; integrated permission-request rendering in StreamMessage via normalized tool-name checks.
Type Definitions
components/frontend/src/types/agentic-session.ts
Added exported PermissionRequestInput type and exported hasToolResult(resultBlock?: ToolResultBlock): boolean for consistent result-presence checks.
Runtime Adapter (Claude)
components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
Added permission-gating: _PERM_* sentinel IDs, _approved_operations set, set_permission_worker(), can_use_tool callback that injects synthetic PermissionRequest tool events to halt execution, and logic to apply approvals when perm-prefixed tool-call responses arrive.
Bridge & Session Worker
components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py, components/runners/ambient-runner/ambient_runner/bridges/claude/session.py
Bridge now binds/unbinds the active worker to the adapter before/after runs via set_permission_worker. SessionWorker exposes active_output_queue property for adapter event injection and inspection.
Agent Status & Hooks
components/frontend/src/hooks/use-agent-status.ts, components/frontend/src/components/ui/stream-message.tsx
Introduced normalizeToolName and replaced isAskUserQuestionTool with a generalized predicate (isHumanInTheLoopTool / isPermissionRequestTool), expanding waiting-input detection to include permission-request tool calls.

Sequence Diagram(s)

sequenceDiagram
    participant Claude as Claude SDK
    participant Adapter as ClaudeAgentAdapter
    participant WorkerQueue as SessionWorker Output Queue
    participant Frontend as Frontend UI
    participant User as User

    Claude->>Adapter: can_use_tool(tool_call)
    Adapter->>Adapter: compute permission_key (_PERM prefix)
    alt key in _approved_operations
        Adapter->>Claude: allow tool usage
    else not approved
        Adapter->>WorkerQueue: inject synthetic PermissionRequest (ToolCallStart/Args/End)
        Adapter->>Claude: deny tool usage
        WorkerQueue->>Frontend: deliver PermissionRequest event
        Frontend->>Frontend: render PermissionRequestMessage
        Frontend->>User: display prompt (Allow/Deny)
        User->>Frontend: respond (approved/denied)
        Frontend->>WorkerQueue: submit JSON response (approved, key)
        WorkerQueue->>Adapter: deliver permission response
        Adapter->>Adapter: update _approved_operations if approved
        Adapter->>Claude: retry tool call (allow/deny)
    end
    Claude->>Claude: execute tool (if allowed)
Loading

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Performance And Algorithmic Complexity ❌ Error _approved_operations set in ClaudeAgentAdapter exhibits unbounded growth with no eviction, TTL, or size limits, accumulating indefinitely across bridge lifetime. Implement LRU cache with max size, TTL-based expiration, or session-scoped clearing with explicit termination handling.
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title follows Conventional Commits format (feat: ) and accurately summarizes the main changeset: adding a permission request UI component for sensitive tool operations.
Security And Secret Handling ✅ Passed PR securely handles permissions without hardcoded secrets, command injection, or unsafe input processing. API keys use environment variables, inputs are safely validated, and event stream wraps with secret redaction middleware.
Kubernetes Resource Safety ✅ Passed PR contains only frontend React components, TypeScript utilities, and Python backend code with no Kubernetes manifests.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/permission-request-ui
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/permission-request-ui

Comment @coderabbitai help to get the list of available commands and usage tips.

- Remove hasResult alias, use hasToolResult directly
- Collapse config/resolvedConfig/activeConfig triple to single lookup
- Remove redundant hasattr/isinstance guards on ToolCallEndEvent
- Use getattr for placeholder ID checks
- Clear _permission_worker in bridge finally block (prevent GC leak)
- Remove dead description initializer
- Add PermissionRequest to use-agent-status.ts waiting_input detection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py`:
- Around line 976-992: The conditional uses
message.tool_call_id.startswith(_PERM_TOOL_ID_PREFIX) without guarding against
tool_call_id being None, which can raise AttributeError for ToolCallEndEvent
instances missing tool_call_id; update the if condition in the block handling
ToolCallEndEvent to first verify message.tool_call_id is not None (or truthy)
before calling startswith, then proceed to call flush_pending_msg(), set
self._halted, self._halted_tool_call_id and halt_event_stream as currently
implemented when the guarded check passes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 019bb7d7-c398-4f4e-a5ff-c25149531d0b

📥 Commits

Reviewing files that changed from the base of the PR and between bfcd54e and 0458999.

📒 Files selected for processing (5)
  • components/frontend/src/components/session/ask-user-question.tsx
  • components/frontend/src/components/session/permission-request.tsx
  • components/frontend/src/hooks/use-agent-status.ts
  • components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
  • components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/frontend/src/components/session/permission-request.tsx

Comment thread components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions bot added ambient-code:needs-human AI circuit breaker fired, needs human attention and removed ambient-code:managed PR managed by AI automation labels Apr 3, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

AI was unable to resolve issues after 3 attempts. Needs human attention.

@ambient-code ambient-code bot added this to the Review Queue milestone Apr 6, 2026
@Gkrumbach07
Copy link
Copy Markdown
Contributor Author

@ambient-code please fix

Performance And Algorithmic Complexity | ❌ Error | The _approved_operations set persists across runs without eviction, accumulating indefinitely as the adapter is intentionally cached and reused. | Implement LRU cache with fixed size (e.g., max 1000 entries), add TTL-based auto-eviction, or clear at session boundaries; document approval persistence scope.

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 17, 2026

Deploy Preview for cheerful-kitten-f556a0 canceled.

Name Link
🔨 Latest commit 23dd3ef
🔍 Latest deploy log https://app.netlify.com/projects/cheerful-kitten-f556a0/deploys/69e1b7d45f76c10007126396

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`:
- Around line 267-270: The adapter’s global permission-worker is being set and
cleared via self._adapter.set_permission_worker(worker) while only locking per
thread_id, causing cross-session races; change the binding to be scoped by
thread_id (or task-local context) so the adapter stores & retrieves permission
workers keyed by thread_id instead of a single global slot, use
self._session_manager.get_lock(thread_id) to guard only that key, set the keyed
binding at the start of the run (where set_permission_worker(worker) is called)
and clear only that key in the finally block (matching the existing clear at
lines around 324-325), and update adapter methods (e.g.,
set_permission_worker/get_permission_worker or similar) to accept a thread_id
param so injection and clearing affect only the current session.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 281652d6-2ca8-43c4-9bb9-ac0fa71435c7

📥 Commits

Reviewing files that changed from the base of the PR and between 7969e3e and edac128.

📒 Files selected for processing (2)
  • components/frontend/src/types/agentic-session.ts
  • components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/frontend/src/types/agentic-session.ts

Comment on lines 267 to +270
async with self._session_manager.get_lock(thread_id):
# Expose the worker to the adapter so the can_use_tool callback
# can inject synthetic events into the active output queue.
self._adapter.set_permission_worker(worker)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Shared permission-worker state is unsafe across concurrent sessions (critical).

Line 267 serializes only per thread_id, but Lines 270 and 325 mutate a single adapter-global permission worker reference. With concurrent runs on different thread IDs, one run can overwrite or clear another run’s worker, so permission events may be injected into the wrong session queue or dropped mid-run.

Scope permission-worker binding per run/session (e.g., keyed by thread_id or task-local context), and clear only that scoped binding in finally.

As per coding guidelines, "Flag only errors, security risks, or functionality-breaking problems."

Also applies to: 324-325

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`
around lines 267 - 270, The adapter’s global permission-worker is being set and
cleared via self._adapter.set_permission_worker(worker) while only locking per
thread_id, causing cross-session races; change the binding to be scoped by
thread_id (or task-local context) so the adapter stores & retrieves permission
workers keyed by thread_id instead of a single global slot, use
self._session_manager.get_lock(thread_id) to guard only that key, set the keyed
binding at the start of the run (where set_permission_worker(worker) is called)
and clear only that key in the finally block (matching the existing clear at
lines around 324-325), and update adapter methods (e.g.,
set_permission_worker/get_permission_worker or similar) to accept a thread_id
param so injection and clearing affect only the current session.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ambient-code:needs-human AI circuit breaker fired, needs human attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant