- Promote [Unreleased] CHANGELOG entries to [3.8.0] (2026-04-05) - Add Shell Danger Detection section to docs/api/tui.md covering ShellDangerLevel, DangerousPattern, ShellSafetyService, and SafetyCheckResult with full API reference and usage examples - Add InvariantService section to docs/api/core.md documenting the new DI-registered singleton, its methods, and emitted events - Update docs/architecture.md Plan Lifecycle section to document invariant reconciliation as a phase transition gate - Update README.md Highlights with shell danger detection, inline permission questions, invariant reconciliation, UKO provenance tracking, and JSON-RPC 2.0 A2A wire format - Create docs/modules/shell-safety.md with full module guide covering purpose, key classes, built-in patterns, custom pattern registration, TUI integration, and testing guidance ISSUES CLOSED: #1003 #997 #1391 #1004 #891 #1501 #1577 #1941 #2334
11 KiB
cleveragents.tui — Interactive Terminal UI
The tui package provides the full-screen Textual-based terminal user interface.
It requires the optional cleveragents[tui] extra.
See ADR-044 (TUI framework), ADR-045 (persona system), and ADR-046 (reference and command system) for design rationale.
Launching the TUI
pip install "cleveragents[tui]"
agents tui
First-Run Experience
is_first_run(registry: PersonaRegistry) → bool
Returns True when no personas are configured — used by app.on_mount() to
decide whether to show the actor selection overlay.
create_default_persona_for_actor(registry: PersonaRegistry, actor: str) → None
Creates and persists a "default" persona bound to actor after the user
completes the first-run selection flow.
ActorSelectionOverlay
from cleveragents.tui.widgets import ActorSelectionOverlay
A centred Textual widget displayed on first launch.
| Method | Description |
|---|---|
show() |
Make the overlay visible |
hide() |
Dismiss the overlay |
move_up() |
Move selection cursor up (wraps) |
move_down() |
Move selection cursor down (wraps) |
set_search(query: str) |
Apply fuzzy filter to actor list |
confirm() → str |
Confirm selection; returns actor name and hides overlay |
render_actor_selection() → str |
Pure rendering function (testable without Textual) |
Default actor list (in display order):
| Actor | Notes |
|---|---|
anthropic/claude-4-sonnet |
Recommended |
anthropic/claude-4-opus |
|
openai/gpt-4o |
|
openai/o3 |
|
google/gemini-2 |
Key bindings inside the overlay:
| Key | Action |
|---|---|
j / ↓ |
Move down |
k / ↑ |
Move up |
/ |
Enter fuzzy search |
Enter |
Confirm selection |
Persona System
Personas are YAML files stored in ~/.config/cleveragents/personas/. Each
persona binds an actor, optional argument presets, and scope references to a
named identity.
PersonaRegistry
from cleveragents.tui.persona import PersonaRegistry
registry = PersonaRegistry()
registry.load() # load all YAML files from config dir
persona = registry.get("default") # retrieve by name
registry.save(persona) # persist changes
registry.ensure_default(actor="openai/gpt-4o") # create default if absent
Persona
@dataclass
class Persona:
name: str
actor: str
presets: list[ArgumentPreset]
scope_refs: list[str]
Input Mode Routing
The prompt auto-detects three input modes from the first character:
| First character | Mode | Handler |
|---|---|---|
| (none / letter) | Normal | Message + @reference expansion |
/ |
Command | Slash command overlay |
! |
Shell | Subprocess passthrough |
InputModeRouter
from cleveragents.tui.routing import InputModeRouter
router = InputModeRouter(container)
await router.dispatch(input_text, session_id)
Slash Commands
67 slash commands across 14 groups are exposed via SlashCommandOverlay.
from cleveragents.tui.commands import SLASH_COMMAND_SPECS
# SLASH_COMMAND_SPECS: dict[str, list[SlashCommandSpec]]
# Keys are group names; values are lists of command specs.
Groups: Session, Persona, Scope, Plan, Project, Actor, Resource, Config, Tool, Skill, Invariant, Profile, Context, Utility.
Session commands (via TuiCommandRouter)
| Command | Description |
|---|---|
/session:export [--format json|md] [path] |
Export session to JSON or Markdown |
/session:import <path> |
Import a session from a JSON file |
# Export as Markdown transcript
/session:export --format md ~/my-session.md
# Export as canonical JSON (default)
/session:export ~/my-session.json
# Import a previously exported session
/session:import ~/my-session.json
Session Export / Import
Session.as_export_markdown() → str
Domain method on Session that renders a human-readable Markdown transcript.
The output is lossy (for sharing/documentation) and cannot be re-imported.
from cleveragents.domain.models.core.session import Session
session: Session = ...
md = session.as_export_markdown()
# Returns a Markdown string with:
# - Header block: session ID, actor, created_at, message count
# - Message history: role | timestamp | content
# - Linked plan references
CLI
# Export as canonical JSON (importable)
agents session export --session-id <ID> --output session.json
# Export as Markdown transcript (human-readable, not importable)
agents session export --session-id <ID> --output session.md --format md
# Import from JSON
agents session import --input session.json
Widgets
ThoughtBlockWidget
Renders actor reasoning traces inline in the conversation stream.
from cleveragents.tui.widgets import ThoughtBlockWidget
- Collapsed by default; press
Spaceto expand/collapse - Muted styling distinguishes thought blocks from regular messages
- Backed by
ThoughtBlockdomain model with configurablemax_lines(default: 10)
PermissionQuestionWidget
Inline permission question widget rendered directly in the conversation stream
for single-file permission requests. For multi-file operations the full
PermissionsScreen is pushed instead.
from cleveragents.tui.widgets import PermissionQuestionWidget
from cleveragents.domain.models.core.inline_permission_question import (
InlinePermissionQuestion,
PermissionDecision,
)
widget = PermissionQuestionWidget(question)
event = widget.handle_key("a") # returns PermissionDecisionEvent or None
| Method | Description |
|---|---|
move_up() |
Move selection cursor up (wraps) |
move_down() |
Move selection cursor down (wraps) |
handle_key(key: str) → PermissionDecisionEvent | None |
Process a key press; returns a decision event when resolved |
Key bindings:
| Key | Action |
|---|---|
a |
Allow once |
A |
Allow always (this session) |
r |
Reject once |
R |
Reject always (this session) |
↑ / ↓ |
Navigate options |
Enter |
Confirm highlighted option |
v |
Open full PermissionsScreen with diff view |
PermissionDecisionEvent — emitted when the user makes a decision:
@dataclass
class PermissionDecisionEvent:
question: InlinePermissionQuestion
decision: PermissionDecision
render_permission_question(question, selected_index=0, *, show_diff=False) → str — pure rendering helper (testable without Textual).
PermissionsScreen
Full-screen overlay for tool permission requests.
from cleveragents.tui.screens import PermissionsScreen
| Key | Action |
|---|---|
a |
Allow once |
A |
Allow always |
r |
Reject once |
R |
Reject always |
d |
Cycle diff display mode (unified → side-by-side → context) |
Shell Danger Detection
Module: cleveragents.tui.shell_safety
The TUI shell mode (! prefix) checks commands against a configurable pattern
registry before execution. Commands that match a dangerous pattern surface a
warning overlay; commands at or above the configured block_level are blocked
automatically unless a warn_callback is provided.
ShellDangerLevel
from cleveragents.tui.shell_safety import ShellDangerLevel
IntEnum with four severity levels (ordered least to most severe):
| Level | Value | Description |
|---|---|---|
LOW |
1 | Minor risk — generally recoverable (e.g. chmod 777 on a single file) |
MEDIUM |
2 | Moderate risk — can cause data loss or security exposure (e.g. sudo rm, wget | sh) |
HIGH |
3 | High risk — significant, hard-to-reverse damage (e.g. dd if=, mkfs) |
CRITICAL |
4 | Critical risk — can destroy the system or create a fork bomb (e.g. rm -rf /, :(){ :|:& };:) |
Numeric comparisons work naturally: level >= ShellDangerLevel.HIGH.
DangerousPattern
from cleveragents.tui.shell_safety import DangerousPattern
pattern = DangerousPattern(
name="rm_rf_root",
pattern=r"rm\s+-[rRfF]*\s+/",
level=ShellDangerLevel.CRITICAL,
description="Recursively deletes the root filesystem.",
)
Immutable frozen dataclass describing a single dangerous shell pattern.
| Attribute | Type | Description |
|---|---|---|
name |
str |
Short human-readable identifier |
pattern |
str |
Regex matched against the command text |
level |
ShellDangerLevel |
Severity classification |
description |
str |
Human-readable explanation of the risk |
case_sensitive |
bool |
When True, regex is compiled without re.IGNORECASE (default: False) |
matches(command: str) → bool — returns True if command matches the compiled pattern.
ShellSafetyService
from cleveragents.tui.shell_safety import ShellSafetyService, ShellDangerLevel
service = ShellSafetyService(block_level=ShellDangerLevel.MEDIUM)
result = service.check_command("rm -rf /")
if not result.allowed:
print(result.warning.message)
Application service that checks shell commands before execution.
Constructor parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
detector |
DangerousPatternDetector | None |
None |
Custom detector; a default one is created when None |
block_level |
ShellDangerLevel |
MEDIUM |
Minimum level that triggers automatic blocking when no callback is provided |
warn_callback |
Callable[[DangerousCommandWarning], bool] | None |
None |
Optional callback invoked for dangerous commands; return True to allow, False to block |
extra_patterns |
list[DangerousPattern] | None |
None |
Additional patterns registered on top of the defaults |
Methods:
| Method | Returns | Description |
|---|---|---|
check_command(command) |
SafetyCheckResult |
Check a command and return a structured result |
is_safe(command) |
bool |
Convenience wrapper — returns True if the command passes |
SafetyCheckResult attributes:
| Attribute | Type | Description |
|---|---|---|
command |
str |
The command that was checked |
warning |
DangerousCommandWarning | None |
Warning if a dangerous pattern matched, otherwise None |
allowed |
bool |
True if the command is allowed to proceed |
Built-in pattern categories:
- Destructive filesystem operations (
rm -rf,shred,wipe) - Privilege escalation (
sudo,su,chmod 777 /) - Network exfiltration (
curl \| sh,wget \| bash,ncreverse shells) - Fork bombs and resource exhaustion
- Disk-level writes (
dd if=,mkfs)
TUI State Persistence
The TUI persists minimal state to ~/.config/cleveragents/tui-state.yaml:
last_persona: "default"
This is loaded on startup to restore the last active persona.