Files
freemo 0d4c5b6ff1
CI / lint (push) Failing after 24s
CI / build (push) Successful in 14s
CI / helm (push) Successful in 25s
CI / typecheck (push) Successful in 49s
CI / coverage (push) Has been skipped
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Failing after 1m45s
CI / integration_tests (push) Has been cancelled
CI / security (push) Has been cancelled
CI / quality (push) Has been cancelled
CI / e2e_tests (push) Has been cancelled
CI / benchmark-publish (push) Has been cancelled
CI / status-check (push) Has been cancelled
CI / docker (push) Has been cancelled
docs: add v3.7.0 cycle-2 documentation for UKO runtime, TUI permissions, database handler, and thought blocks
- 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
2026-04-02 17:35:41 +00:00

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)