- CHANGELOG.md: extend [3.7.0] section with 15 new entries covering PermissionsScreen, ThoughtBlock, UKO runtime services, ACMS Phase 2 protocol aliases, DevcontainerHandler protocol completion, DatabaseResourceHandler CRUD/checkpoint, estimation lifecycle hook, user_identity domain events, PLAN_APPLIED/PLAN_CANCELLED enrichment, server serve subcommand, async audit refactor, and CLI flag fixes - README.md: extend Highlights with permissions screen, thought blocks, UKO runtime, database handler, and estimation lifecycle - docs/reference/uko_runtime.md: new reference for UKOQueryInterface, UKOInferenceEngine, and UKOGraphPersistence (issue #891) - docs/reference/tui_permissions.md: new reference for PermissionsScreen, PermissionRequestService, and all domain models (issue #996) - docs/reference/tui_thought_block.md: new reference for ThoughtBlock domain model and ThoughtBlockWidget (issue #1001) - docs/reference/database_handler.md: new reference for DatabaseResourceHandler CRUD and checkpoint methods (issue #1241) - docs/reference/devcontainer_resources.md: add DevcontainerHandler protocol methods section (delete, list_children, diff, create_sandbox) (issue #1242) - docs/reference/tui.md: extend architecture table and add Permissions Screen and Actor Thought Blocks sections ISSUES CLOSED: #1393
8.4 KiB
TUI — Permissions Screen
The Permissions Screen is a full-screen TUI overlay that presents tool permission requests to the user before a tool is allowed to modify resources. It displays a file list on the left and a diff view on the right, and records the user's decision for the current session.
Introduced in v3.7.0 (issue #996). See also tui.md for the
overall TUI architecture.
Module: cleveragents.tui.permissions
Overview
When an actor requests a write operation on a resource, the TUI raises the
PermissionsScreen overlay. The user reviews the proposed changes and
decides whether to allow or reject the operation.
┌─────────────────────────────────────────────────────────────────┐
│ Tool: local/file-write Resource: local/api-service │
├──────────────────────┬──────────────────────────────────────────┤
│ Files │ Diff view │
│ ───── │ ───────── │
│ M src/api/main.py │ --- a/src/api/main.py │
│ A src/api/utils.py │ +++ b/src/api/main.py │
│ │ @@ -10,6 +10,8 @@ │
│ │ def run(): │
│ │ + logger.info("starting") │
│ │ + setup_tracing() │
│ │ app.start() │
├──────────────────────┴──────────────────────────────────────────┤
│ [a] Allow once [A] Allow always [r] Reject once [R] Reject │
│ [d] Toggle diff mode (unified / side-by-side / context) │
└─────────────────────────────────────────────────────────────────┘
Keyboard Bindings
| Key | Action |
|---|---|
a |
Allow this request once |
A |
Allow all requests from this tool for the session |
r |
Reject this request once |
R |
Reject all requests from this tool for the session |
d |
Cycle diff display mode (unified → side-by-side → context) |
↑ / ↓ |
Navigate the file list |
Domain Models
DiffDisplayMode
StrEnum controlling how diffs are rendered.
| Value | Description |
|---|---|
unified |
Standard unified diff with +/- lines (default) |
side_by_side |
Two-column view — old content left, new content right |
context |
Shows only changed lines with 3 lines of surrounding context |
FileChangeType
StrEnum for the type of file change.
| Value | Description |
|---|---|
M |
File was modified |
A |
File was added |
D |
File was deleted |
PermissionDecision
StrEnum for the user's decision.
| Value | Description |
|---|---|
allow_once |
Allow this operation once |
allow_always |
Allow all operations from this tool for the session |
reject_once |
Reject this operation once |
reject_always |
Reject all operations from this tool for the session |
PermissionRequest
Pydantic model representing a single file change within a tool permission request.
| Field | Type | Description |
|---|---|---|
path |
str |
File path being changed (non-empty) |
change_type |
FileChangeType |
Type of change (M/A/D) |
before_content |
str | None |
Content before the change (None for new files) |
after_content |
str | None |
Content after the change (None for deleted files) |
Diff methods:
| Method | Returns | Description |
|---|---|---|
unified_diff(*, context_lines=3) |
str |
Standard unified diff |
side_by_side_diff() |
tuple[list[str], list[str]] |
(left_lines, right_lines) |
context_diff(*, context_lines=3) |
str |
Context diff |
render_diff(mode, *, context_lines=3) |
str |
Render in the specified mode |
ToolPermissionRequest
Pydantic model for a complete tool permission request with multiple file changes.
| Field | Type | Description |
|---|---|---|
request_id |
str |
Unique identifier for this request |
tool_name |
str |
Name of the tool requesting permission |
resource_name |
str |
Name of the resource being modified |
changes |
list[PermissionRequest] |
File changes in this request |
decision |
PermissionDecision | None |
User decision (None if pending) |
Properties:
| Property | Type | Description |
|---|---|---|
is_pending |
bool |
True if no decision has been made |
is_allowed |
bool |
True if the request was allowed |
is_rejected |
bool |
True if the request was rejected |
Methods:
| Method | Returns | Description |
|---|---|---|
apply_decision(decision) |
ToolPermissionRequest |
Returns a new request with the decision applied |
PermissionRequestService
Module: cleveragents.tui.permissions.service
In-memory service that manages the permission request queue and persists
session-scoped decisions (allow_always / reject_always).
Methods
| Method | Returns | Description |
|---|---|---|
enqueue(request) |
None |
Add a request to the queue; auto-resolves if a session decision exists |
get(request_id) |
ToolPermissionRequest | None |
Return a request by ID |
pending_requests() |
list[ToolPermissionRequest] |
All undecided requests |
all_requests() |
list[ToolPermissionRequest] |
All requests (pending and decided) |
record_decision(request_id, decision) |
ToolPermissionRequest | None |
Apply a decision; records session-scoped decisions for allow_always / reject_always |
get_session_decision(tool_name) |
PermissionDecision | None |
Return the session-scoped decision for a tool |
clear_session_decision(tool_name) |
bool |
Remove a session-scoped decision |
clear_all() |
None |
Clear all requests and session decisions |
Session-Scoped Decisions
When the user presses A (allow-always) or R (reject-always), the
PermissionRequestService records the decision against the tool name.
Subsequent requests from the same tool are auto-resolved immediately when
enqueue() is called, without showing the PermissionsScreen.
PermissionsScreen Widget
Module: cleveragents.tui.permissions.screen
The PermissionsScreen class is a Textual Screen subclass that renders
the split-pane layout. It is instantiated by the main TUI app when a
ToolPermissionRequest arrives.
Diff Mode Cycling
The diff display mode cycles through unified → side_by_side → context
each time the user presses d. The current mode is displayed in the
status bar.
Usage Example
from cleveragents.tui.permissions.models import (
DiffDisplayMode,
FileChangeType,
PermissionDecision,
PermissionRequest,
ToolPermissionRequest,
)
from cleveragents.tui.permissions.service import PermissionRequestService
service = PermissionRequestService()
# Build a permission request
request = ToolPermissionRequest(
request_id="req-001",
tool_name="local/file-write",
resource_name="local/api-service",
changes=[
PermissionRequest(
path="src/api/main.py",
change_type=FileChangeType.MODIFIED,
before_content="def run():\n app.start()\n",
after_content="def run():\n logger.info('starting')\n app.start()\n",
)
],
)
# Enqueue the request
service.enqueue(request)
# Render the diff
change = request.changes[0]
print(change.render_diff(DiffDisplayMode.UNIFIED))
# Record a decision
service.record_decision("req-001", PermissionDecision.ALLOW_ONCE)
Related Documentation
- TUI Reference — overall TUI architecture and key bindings
- Permissions — permission system and role bindings
- Safety Profiles — automation profile safety levels