Files
owlen/PERMISSION_SYSTEM.md

312 lines
11 KiB
Markdown

# 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<String>,
}
```
Stores information about tool awaiting permission.
2. **TuiApp fields:**
- `pending_tool: Option<PendingToolCall>` - Current pending tool
- `permission_tx: Option<oneshot::Sender<bool>>` - 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<String>
```
**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.