Compare commits

...

1 Commits

Author SHA1 Message Date
freemo 815c28c8a3 docs(api): add shell safety module docs and domain base model page
CI / benchmark-publish (pull_request) Has been skipped
CI / helm (pull_request) Successful in 29s
CI / lint (pull_request) Successful in 3m19s
CI / build (pull_request) Successful in 3m18s
CI / quality (pull_request) Successful in 3m48s
CI / typecheck (pull_request) Successful in 3m56s
CI / security (pull_request) Successful in 4m12s
CI / unit_tests (pull_request) Failing after 6m37s
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Failing after 16m34s
CI / coverage (pull_request) Successful in 14m21s
CI / integration_tests (pull_request) Failing after 21m44s
CI / status-check (pull_request) Failing after 1s
CI / benchmark-regression (pull_request) Successful in 55m52s
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.
2026-04-03 17:22:51 +00:00
4 changed files with 323 additions and 1 deletions
+167
View File
@@ -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
```
+2 -1
View File
@@ -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
+153
View File
@@ -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`:
+1
View File
@@ -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