From 815c28c8a34a46d539d15f4965507d49cc4b0ad4 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Fri, 3 Apr 2026 17:22:51 +0000 Subject: [PATCH] docs(api): add shell safety module docs and domain base model page Added Shell Safety section to docs/api/tui.md documenting the new cleveragents.tui.shell_safety subsystem introduced in the Unreleased changelog: ShellDangerLevel, DangerousPattern, DangerousPatternDetector, ShellSafetyService, and SafetyCheckResult with the full DEFAULT_PATTERNS table and usage examples. Created docs/api/domain.md as a new canonical API page for the domain layer, covering DomainBaseModel (cleveragents.domain.models.base), ThoughtBlock, InlinePermissionQuestion, PermissionDecision, and PermissionDecisionEvent, plus a sub-package index for all core domain models. Updated docs/api/index.md module table and mkdocs.yml nav to include the new Domain Models page. --- docs/api/domain.md | 167 +++++++++++++++++++++++++++++++++++++++++++++ docs/api/index.md | 3 +- docs/api/tui.md | 153 +++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 docs/api/domain.md diff --git a/docs/api/domain.md b/docs/api/domain.md new file mode 100644 index 000000000..3ea77ef12 --- /dev/null +++ b/docs/api/domain.md @@ -0,0 +1,167 @@ +# `cleveragents.domain` — Domain Models + +The `domain` package contains all domain-layer Pydantic models, value objects, +and enumerations used throughout the CleverAgents platform. Domain models are +pure data structures with no infrastructure dependencies — they may be freely +imported by any layer. + +See [ADR-001](../adr/ADR-001-layered-architecture.md) for the layered +architecture rules that govern this package. + +--- + +## Base Model + +### `DomainBaseModel` + +**Module:** `cleveragents.domain.models.base` + +```python +from cleveragents.domain.models.base import DomainBaseModel +from pydantic import Field + +class MyModel(DomainBaseModel): + name: str + count: int = Field(default=0, ge=0) +``` + +Shared Pydantic base class for all standard domain-layer models. Inherit +from `DomainBaseModel` instead of `pydantic.BaseModel` directly to ensure +the shared `model_config` is applied consistently. + +**`model_config` settings:** + +| Setting | Value | Effect | +|---------|-------|--------| +| `str_strip_whitespace` | `True` | Leading/trailing whitespace is stripped from all `str` fields on assignment and validation | +| `validate_assignment` | `True` | Field assignments after construction are validated just like constructor arguments | +| `arbitrary_types_allowed` | `False` | All field types must be Pydantic-compatible; no raw Python objects | +| `populate_by_name` | `True` | Models can be constructed using either the Python field name or the JSON alias | +| `use_enum_values` | `True` | Enum fields are stored and serialised as their underlying primitive values | + +**Why inherit from `DomainBaseModel`?** + +Prior to its introduction, the same `model_config` block was duplicated +across 14+ domain model files. `DomainBaseModel` centralises this +configuration so that any future change propagates automatically to every +consumer without a mass-edit. + +**Example — whitespace stripping:** + +```python +from cleveragents.domain.models.base import DomainBaseModel + +class PlanRecord(DomainBaseModel): + title: str + +record = PlanRecord(title=" My Plan ") +assert record.title == "My Plan" # whitespace stripped automatically +``` + +**Example — assignment validation:** + +```python +record.title = " Updated " +assert record.title == "Updated" # validated and stripped on assignment +``` + +--- + +## Core Domain Models + +The following sub-packages contain the primary domain models. Each is +documented in the relevant API or reference page. + +| Sub-package | Description | +|-------------|-------------| +| `domain.models.core.plan` | `LifecyclePlan`, `PlanPhase`, `PlanState` | +| `domain.models.core.session` | `Session`, `Message` | +| `domain.models.core.actor` | `ActorConfig`, `ActorSpec` | +| `domain.models.core.decision` | `Decision`, `DecisionType` | +| `domain.models.core.resource` | `Resource`, `ResourceSpec` | +| `domain.models.core.skill` | `Skill`, `SkillSpec` | +| `domain.models.core.tool` | `Tool`, `ToolSpec` | +| `domain.models.core.automation_profile` | `AutomationProfile` | +| `domain.models.core.safety_profile` | `SafetyProfile` | +| `domain.models.core.invariant` | `Invariant`, `InvariantResult` | +| `domain.models.core.correction` | `CorrectionAttemptRecord`, `CorrectionAttemptState` | +| `domain.models.core.estimation` | `EstimationResult` | +| `domain.models.core.permission` | `PermissionRequest`, `PermissionDecision` | +| `domain.models.core.inline_permission_question` | `InlinePermissionQuestion`, `PermissionDecisionEvent` | +| `domain.models.thought.thought_block` | `ThoughtBlock` | +| `domain.models.acms` | ACMS context fragment and tier models | + +> **Note:** All models in `domain.models.core` and `domain.models.thought` +> inherit from `DomainBaseModel` and therefore share the configuration +> described above. + +--- + +## ThoughtBlock + +**Module:** `cleveragents.domain.models.thought.thought_block` + +```python +from cleveragents.domain.models.thought.thought_block import ThoughtBlock + +block = ThoughtBlock(content="Reasoning trace...", max_lines=10) +block.toggle() # expand / collapse +block.is_expanded # bool +block.truncated_content # first max_lines lines +block.full_content # complete content +``` + +Represents an actor reasoning trace rendered inline in the TUI conversation +stream. + +| Attribute / Method | Type | Description | +|--------------------|------|-------------| +| `content` | `str` | Full reasoning trace text | +| `max_lines` | `int` | Maximum lines shown when collapsed (default: 10) | +| `is_expanded` | `bool` | Whether the block is currently expanded | +| `truncated_content` | `str` | First `max_lines` lines of `content` | +| `full_content` | `str` | Alias for `content` | +| `toggle()` | — | Toggle between expanded and collapsed | + +--- + +## InlinePermissionQuestion + +**Module:** `cleveragents.domain.models.core.inline_permission_question` + +```python +from cleveragents.domain.models.core.inline_permission_question import ( + InlinePermissionQuestion, + PermissionDecision, + PermissionDecisionEvent, +) +``` + +Value object representing a single-file permission request surfaced inline +in the TUI conversation stream. + +| Field | Type | Description | +|-------|------|-------------| +| `tool_name` | `str` | Name of the tool requesting permission | +| `file_path` | `str` | Path of the file being accessed | +| `operation` | `str` | Requested operation (e.g. `"read"`, `"write"`) | +| `diff` | `str \| None` | Optional unified diff of proposed changes | + +**`PermissionDecision`** — enum of possible user decisions: + +| Value | Meaning | +|-------|---------| +| `ALLOW_ONCE` | Allow this specific request | +| `ALLOW_ALWAYS` | Allow all future requests from this tool in this session | +| `REJECT_ONCE` | Reject this specific request | +| `REJECT_ALWAYS` | Reject all future requests from this tool in this session | + +**`PermissionDecisionEvent`** — emitted by `PermissionQuestionWidget` when +the user resolves a permission request: + +```python +@dataclass +class PermissionDecisionEvent: + question: InlinePermissionQuestion + decision: PermissionDecision +``` diff --git a/docs/api/index.md b/docs/api/index.md index 0759783b3..9bbab2ea6 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -9,6 +9,7 @@ classes, functions, and exceptions with signatures and usage examples. | Module | Description | |--------|-------------| | [`cleveragents.core`](core.md) | Exception hierarchy, error handling, retry patterns, async cleanup | +| [`cleveragents.domain`](domain.md) | Domain base model, core domain models, value objects, and enumerations | | [`cleveragents.a2a`](a2a.md) | Agent-to-Agent (A2A) protocol facade, transport, models, and events | | [`cleveragents.actor`](actor.md) | Actor registry, configuration, loader, and compiler | | [`cleveragents.skills`](skills.md) | Skill framework — schema, protocol, registry, discovery, and inline executor | @@ -16,7 +17,7 @@ classes, functions, and exceptions with signatures and usage examples. | [`cleveragents.mcp`](mcp.md) | Model Context Protocol (MCP) adapter, client, registry, and sandbox | | [`cleveragents.resource`](resource.md) | Resource schema, handlers, and type-inheritance system | | [`cleveragents.config`](config.md) | Application settings, logging, metrics, and security scanning | -| [`cleveragents.tui`](tui.md) | Interactive Terminal UI — app, persona system, input routing, slash commands, session export/import | +| [`cleveragents.tui`](tui.md) | Interactive Terminal UI — app, persona system, input routing, slash commands, shell safety, session export/import | > **Note:** Internal modules (prefixed with `_`) and implementation details > not listed in a module's `__all__` are considered private and may change diff --git a/docs/api/tui.md b/docs/api/tui.md index 7db1f0acc..55e64892a 100644 --- a/docs/api/tui.md +++ b/docs/api/tui.md @@ -269,6 +269,159 @@ from cleveragents.tui.screens import PermissionsScreen --- +## Shell Safety + +**Module:** `cleveragents.tui.shell_safety` + +The shell safety subsystem detects dangerous command patterns before the TUI +executes them in shell mode (`!` prefix). A configurable pattern registry +classifies commands by danger level and surfaces a warning overlay before +proceeding. + +### `ShellDangerLevel` + +```python +from cleveragents.tui.shell_safety import ShellDangerLevel +``` + +`IntEnum` with four ordered severity levels: + +| 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 entire system (e.g. `rm -rf /`, fork bomb) | + +Levels are ordered so numeric comparisons work naturally: +`level >= ShellDangerLevel.HIGH`. + +--- + +### `DangerousPattern` + +```python +from cleveragents.tui.shell_safety import DangerousPattern + +pattern = DangerousPattern( + name="rm_rf_root", + pattern=r"rm\s+(-\w*r\w*f|-\w*f\w*r)\s+/\s*$", + level=ShellDangerLevel.CRITICAL, + description="rm -rf / recursively deletes the entire filesystem root.", +) +pattern.matches("rm -rf /") # True +``` + +Immutable frozen dataclass describing a single dangerous shell pattern. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `name` | `str` | Short human-readable identifier | +| `pattern` | `str` | Regular expression matched against the command | +| `level` | `ShellDangerLevel` | Severity classification | +| `description` | `str` | Human-readable explanation of the risk | +| `case_sensitive` | `bool` | When `True` the regex is case-sensitive (default: `False`) | + +**Method:** `matches(command: str) → bool` — returns `True` if the command +matches this pattern. + +--- + +### `DangerousPatternDetector` + +```python +from cleveragents.tui.shell_safety import DangerousPatternDetector + +detector = DangerousPatternDetector() +warnings = detector.check("rm -rf /") +first = detector.check_first("sudo rm important.txt") +``` + +Checks shell commands against a configurable registry of dangerous patterns. +Ships with `DEFAULT_PATTERNS` but supports full customisation. + +| Method | Description | +|--------|-------------| +| `check(command) → list[DangerousCommandWarning]` | Return all matching warnings | +| `check_first(command) → DangerousCommandWarning \| None` | Return the first (highest-priority) warning | +| `is_dangerous(command) → bool` | Return `True` if any pattern matches | +| `max_danger_level(command) → ShellDangerLevel \| None` | Return the highest matched danger level | +| `add_pattern(pattern)` | Append a pattern to the registry | +| `remove_pattern(name) → bool` | Remove a pattern by name; returns `True` if found | +| `replace_patterns(patterns)` | Replace the entire registry | +| `patterns` | Read-only tuple of current patterns | + +**Built-in patterns** (`DEFAULT_PATTERNS`): + +| Name | Level | Matches | +|------|-------|---------| +| `rm_rf_root` | CRITICAL | `rm -rf /` | +| `rm_rf_wildcard` | CRITICAL | `rm -rf /*` or `rm -rf /path*` | +| `fork_bomb` | CRITICAL | `:(){ :|:& };:` | +| `dd_if_device` | HIGH | `dd if=` | +| `mkfs` | HIGH | `mkfs` | +| `shred_device` | HIGH | `shred /dev/` or `shred --remove` | +| `chmod_777` | MEDIUM | `chmod 777` | +| `sudo_rm` | MEDIUM | `sudo rm` | +| `wget_pipe_sh` | MEDIUM | `wget ... \| sh` | +| `curl_pipe_sh` | MEDIUM | `curl ... \| sh` | +| `wget_pipe_bash` | MEDIUM | `wget ... \| bash` | +| `curl_pipe_bash` | MEDIUM | `curl ... \| bash` | +| `git_push_force` | LOW | `git push --force` | +| `chmod_recursive_permissive` | LOW | `chmod -R 6xx/7xx` | + +--- + +### `ShellSafetyService` + +```python +from cleveragents.tui.shell_safety import ShellSafetyService + +service = ShellSafetyService() +result = service.check_command("rm -rf /") +if not result.allowed: + print(result.warning.message) +``` + +Application service that wraps `DangerousPatternDetector` with a higher-level +API suitable for use by the TUI. + +```python +ShellSafetyService( + detector=None, # custom DangerousPatternDetector (optional) + block_level=ShellDangerLevel.MEDIUM, # auto-block threshold + warn_callback=None, # (warning) -> bool; True = allow, False = block + extra_patterns=None, # additional DangerousPattern objects +) +``` + +| Method / Property | Description | +|-------------------|-------------| +| `check_command(command) → SafetyCheckResult` | Full safety check with allow/block decision | +| `is_safe(command) → bool` | Convenience wrapper; `True` if allowed | +| `detector` | The underlying `DangerousPatternDetector` | +| `block_level` | The minimum level that triggers automatic blocking | + +**`SafetyCheckResult`** attributes: + +| Attribute | Type | Description | +|-----------|------|-------------| +| `command` | `str` | The command that was checked | +| `warning` | `DangerousCommandWarning \| None` | Warning if a pattern matched | +| `allowed` | `bool` | `True` if the command may proceed | + +**Custom warn callback example:** + +```python +def my_callback(warning: DangerousCommandWarning) -> bool: + # Show a UI prompt; return True to allow, False to block + return show_confirmation_dialog(warning.message) + +service = ShellSafetyService(warn_callback=my_callback) +``` + +--- + ## TUI State Persistence The TUI persists minimal state to `~/.config/cleveragents/tui-state.yaml`: diff --git a/mkdocs.yml b/mkdocs.yml index de3458930..074168dfe 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,6 +14,7 @@ nav: - API Reference: - Overview: api/index.md - Core Utilities: api/core.md + - Domain Models: api/domain.md - A2A Protocol: api/a2a.md - Actor System: api/actor.md - Skills: api/skills.md -- 2.52.0