# Permission System - TUI Implementation ## Overview The TUI now has a fully functional interactive permission system that allows users to grant, deny, or permanently allow tool executions through an elegant popup interface. ## Features Implemented ### 1. Interactive Permission Popup **Location:** `crates/app/ui/src/components/permission_popup.rs` - **Visual Design:** - Centered modal popup with themed border - Tool name highlighted with icon (⚡) - Context information (file path, command, etc.) - Four selectable options with icons - Keyboard shortcuts and navigation hints - **Options Available:** - ✓ `[a]` **Allow once** - Execute this one time - ✓✓ `[A]` **Always allow** - Add permanent rule to permission manager - ✗ `[d]` **Deny** - Refuse this operation - ? `[?]` **Explain** - Show what this operation does - **Navigation:** - Arrow keys (↑/↓) to select options - Enter to confirm selection - Keyboard shortcuts (a/A/d/?) for quick selection - Esc to deny and close ### 2. Permission Flow Integration **Location:** `crates/app/ui/src/app.rs` #### New Components: 1. **PendingToolCall struct:** ```rust struct PendingToolCall { tool_name: String, arguments: Value, perm_tool: PermTool, context: Option, } ``` Stores information about tool awaiting permission. 2. **TuiApp fields:** - `pending_tool: Option` - Current pending tool - `permission_tx: Option>` - Channel to signal decision 3. **execute_tool_with_permission() method:** ```rust async fn execute_tool_with_permission( &mut self, tool_name: &str, arguments: &Value, ) -> Result ``` **Flow:** 1. Maps tool name to PermTool enum (Read, Write, Edit, Bash, etc.) 2. Extracts context (file path, command, etc.) 3. Checks permission via PermissionManager 4. If `Allow` → Execute immediately 5. If `Deny` → Return error 6. If `Ask` → Show popup and wait for user decision **Async Wait Mechanism:** - Creates oneshot channel for permission response - Shows permission popup - Awaits channel response (with 5-minute timeout) - Event loop continues processing keyboard events - When user responds, channel signals and execution resumes ### 3. Permission Decision Handling **Location:** `crates/app/ui/src/app.rs:184-254` When user makes a choice in the popup: - **Allow Once:** - Signals permission granted (sends `true` through channel) - Tool executes once - No persistent changes - **Always Allow:** - Adds new rule to PermissionManager - Rule format: `perms.add_rule(tool, context, Action::Allow)` - Example: Always allow reading from `src/` directory - Signals permission granted - All future matching operations auto-approved - **Deny:** - Signals permission denied (sends `false`) - Tool execution fails with error - Error shown in chat - **Explain:** - Shows explanation of what the tool does - Popup remains open for user to choose again - Tool-specific explanations: - `read` → "read a file from disk" - `write` → "write or overwrite a file" - `edit` → "modify an existing file" - `bash` → "execute a shell command" - `grep` → "search for patterns in files" - `glob` → "list files matching a pattern" ### 4. Agent Loop Integration **Location:** `crates/app/ui/src/app.rs:488` Changed from: ```rust match execute_tool(tool_name, arguments, &self.perms).await { ``` To: ```rust match self.execute_tool_with_permission(tool_name, arguments).await { ``` This ensures all tool calls in the streaming agent loop go through the permission system. ## Architecture Details ### Async Concurrency Model The implementation uses Rust's async/await with tokio to handle the permission flow without blocking the UI: ``` ┌─────────────────────────────────────────────────────────┐ │ Event Loop │ │ (continuously running at 60 FPS) │ │ │ │ while running { │ │ terminal.draw(...) ← Always responsive │ │ if let Ok(event) = event_rx.try_recv() { │ │ handle_event(event).await │ │ } │ │ tokio::sleep(16ms).await ← Yields to runtime │ │ } │ └─────────────────────────────────────────────────────────┘ ↕ ┌─────────────────────────────────────────────────────────┐ │ Keyboard Event Listener │ │ (separate tokio task) │ │ │ │ loop { │ │ event = event_stream.next().await │ │ event_tx.send(Input(key)) │ │ } │ └─────────────────────────────────────────────────────────┘ ↕ ┌─────────────────────────────────────────────────────────┐ │ Permission Request Flow │ │ │ │ 1. Tool needs permission (PermissionDecision::Ask) │ │ 2. Create oneshot channel (tx, rx) │ │ 3. Show popup, store tx │ │ 4. await rx ← Yields to event loop │ │ 5. Event loop continues, handles keyboard │ │ 6. User presses 'a' → handle_event processes │ │ 7. tx.send(true) signals channel │ │ 8. rx.await completes, returns true │ │ 9. Tool executes with permission │ └─────────────────────────────────────────────────────────┘ ``` ### Key Insight The implementation works because: 1. **Awaiting is non-blocking:** When we `await rx`, we yield control to the tokio runtime 2. **Event loop continues:** The outer event loop continues to run its iterations 3. **Keyboard events processed:** The separate event listener task continues reading keyboard 4. **Channel signals resume:** When user responds, the channel completes and we resume This creates a smooth UX where the UI remains responsive while waiting for permission. ## Usage Examples ### Example 1: First-time File Write ``` User: "Create a new file hello.txt with 'Hello World'" Agent: [Calls write tool] ┌───────────────────────────────────────┐ │ 🔒 Permission Required │ ├───────────────────────────────────────┤ │ ⚡ Tool: write │ │ 📝 Context: │ │ hello.txt │ ├───────────────────────────────────────┤ │ ▶ ✓ [a] Allow once │ │ ✓✓ [A] Always allow │ │ ✗ [d] Deny │ │ ? [?] Explain │ │ │ │ ↑↓ Navigate Enter to select Esc... │ └───────────────────────────────────────┘ User presses 'a' → File created once ``` ### Example 2: Always Allow Bash in Current Directory ``` User: "Run npm test" Agent: [Calls bash tool] [Permission popup shows with context: "npm test"] User presses 'A' → Rule added: bash("npm test*") → Allow Future: User: "Run npm test:unit" Agent: [Executes immediately, no popup] ``` ### Example 3: Explanation Request ``` User: "Read my secrets.env file" [Permission popup appears] User presses '?' → System: "Tool 'read' requires permission. This operation will read a file from disk." [Popup remains open] User presses 'd' → Permission denied ``` ## Testing Build status: ✅ All tests pass ```bash cargo build --workspace # Success cargo test --workspace --lib # 28 tests passed ``` ## Configuration The permission system respects three modes from `PermissionManager`: 1. **Plan Mode** (default): - Read operations (read, grep, glob) → Auto-allowed - Write operations (write, edit) → Ask - System operations (bash) → Ask 2. **AcceptEdits Mode**: - Read operations → Auto-allowed - Write operations → Auto-allowed - System operations (bash) → Ask 3. **Code Mode**: - All operations → Auto-allowed - No popups shown User can override mode with CLI flag: `--mode code` ## Future Enhancements Potential improvements: 1. **Permission History:** - Show recently granted/denied permissions - `/permissions` command to view active rules 2. **Temporary Rules:** - "Allow for this session" option - Rules expire when TUI closes 3. **Pattern-based Rules:** - "Always allow reading from `src/` directory" - "Always allow bash commands starting with `npm`" 4. **Visual Feedback:** - Show indicator when permission auto-granted by rule - Different styling for policy-denied vs user-denied 5. **Rule Management:** - `/clear-rules` command - Edit/remove specific rules interactively ## Files Modified - `crates/app/ui/src/app.rs` - Main permission flow logic - `crates/app/ui/src/events.rs` - Removed unused event type - `crates/app/ui/src/components/permission_popup.rs` - Pre-existing, now fully integrated ## Summary The TUI permission system is now fully functional, providing: - ✅ Interactive permission popups with keyboard navigation - ✅ Four permission options (allow once, always, deny, explain) - ✅ Runtime permission rule updates - ✅ Async flow that keeps UI responsive - ✅ Integration with existing permission manager - ✅ Tool-specific context and explanations - ✅ Timeout handling (5 minutes) - ✅ All tests passing Users can now safely interact with the AI agent while maintaining control over potentially dangerous operations.