Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
132ccaa
feat(permissions): scaffold permissions package
shoom1 Apr 18, 2026
6ef9dac
feat(permissions): Capability, ResolvedCapability, EXEMPT sentinel
shoom1 Apr 18, 2026
ae2b5e9
feat(permissions): Rule, Effect, RuleSource, AskScope, CheckResult
shoom1 Apr 18, 2026
163b3eb
feat(permissions): PermissionContext + variable substitution
shoom1 Apr 18, 2026
569b751
feat(permissions): Matcher protocol + StringGlobMatcher
shoom1 Apr 18, 2026
78114e5
feat(permissions): PathMatcher with ** glob support
shoom1 Apr 18, 2026
ffccb77
feat(permissions): URLMatcher for http.* capabilities
shoom1 Apr 18, 2026
8d2ad3d
feat(permissions): ShellMatcher for shell.* capabilities
shoom1 Apr 18, 2026
67ae761
feat(permissions): matcher registry + capability-name glob
shoom1 Apr 18, 2026
a4b2a3a
feat(permissions): BUILTIN_RULES default policy
shoom1 Apr 18, 2026
ecf8d96
feat(permissions): load_rules from settings.json permissions section
shoom1 Apr 18, 2026
cb83c61
feat(permissions): append_project_rule with atomic JSON merge
shoom1 Apr 18, 2026
6c2886d
feat(permissions): build_request + parse_response for ask dialog
shoom1 Apr 18, 2026
dab2191
feat(permissions): PermissionEngine init + rule loading + disabled sh…
shoom1 Apr 18, 2026
d0ae502
feat(permissions): engine rule-based allow/deny path
shoom1 Apr 18, 2026
d243545
feat(permissions): engine ask flow (once/session/always/deny)
shoom1 Apr 18, 2026
96a9af4
test(permissions): serialise concurrent ask prompts
shoom1 Apr 18, 2026
9a6f1bf
refactor(permissions): drop try/except scaffolding from __init__.py n…
shoom1 Apr 18, 2026
230c59b
feat(tools): add ToolDefinition.capabilities field + register_tool kw…
shoom1 Apr 18, 2026
2628cf7
feat(tools): declare capabilities on every registered tool
shoom1 Apr 18, 2026
a720c04
feat(permissions): ADK PermissionPlugin
shoom1 Apr 18, 2026
de905c0
feat(permissions): LangGraph wrap_tool_for_permission
shoom1 Apr 18, 2026
110ae22
feat(permissions): construct PermissionEngine in BaseWorkflowManager …
shoom1 Apr 18, 2026
2de1afc
feat(permissions): add permissions + permissions_enabled settings (st…
shoom1 Apr 18, 2026
2bf7950
feat(permissions): ADK manager uses PermissionPlugin
shoom1 Apr 18, 2026
353edc4
feat(permissions): LangGraph builder uses wrap_tool_for_permission
shoom1 Apr 18, 2026
5b2965b
refactor(permissions): delete retired code (PermissionLevel, Confirma…
shoom1 Apr 18, 2026
7c84fc6
docs(permissions): drop transient 'once Task N' phrasing from adapter…
shoom1 Apr 18, 2026
af1c551
docs(permissions): update CLAUDE.md + README.md to describe the new p…
shoom1 Apr 18, 2026
4e929bb
fix(permissions): preserve '*' wildcard in matcher canonicalize + mat…
shoom1 Apr 18, 2026
fc74e9a
fix(permissions): register backend state tools as EXEMPT
shoom1 Apr 18, 2026
0227b2d
feat(permissions): allow memory.* and kb.* by default; register ADK t…
shoom1 Apr 18, 2026
c1c9b77
perf(workflow): offload _ensure_managers_initialized to a worker thread
shoom1 Apr 18, 2026
73ca202
feat(permissions): broaden filesystem.* grants to the parent directory
shoom1 Apr 18, 2026
74c0db1
feat(permissions): show directory-scope target in ask prompt
shoom1 Apr 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ agentic-cli/
│ │ ├── persistence/ # Checkpointers, stores
│ │ └── tools/ # LangChain-compatible wrappers
│ ├── tools/
│ │ ├── registry.py # ToolRegistry, @register_tool, ToolCategory, PermissionLevel
│ │ ├── registry.py # ToolRegistry, @register_tool, ToolCategory
│ │ ├── executor.py # SafePythonExecutor
│ │ ├── knowledge_tools.py # kb_search, kb_ingest, kb_list, kb_read
│ │ ├── arxiv_tools.py # search_arxiv, fetch_arxiv_paper, analyze_arxiv_paper
Expand All @@ -62,7 +62,7 @@ agentic-cli/
│ │ ├── memory_tools.py # save_memory, search_memory + MemoryStore
│ │ ├── planning_tools.py # save_plan, get_plan + PlanStore
│ │ ├── task_tools.py # save_tasks, get_tasks + TaskStore
│ │ ├── hitl_tools.py # request_approval + ApprovalManager, HITLConfig
│ │ ├── reflection_tools.py # save_reflection + ToolReflectionStore
│ │ ├── shell/ # 8-layer shell security
│ │ └── webfetch/ # Fetcher, converter, validator, robots
│ ├── knowledge_base/
Expand Down Expand Up @@ -133,7 +133,8 @@ Workflow:

### Key Design Patterns
- **Tool error handling**: All tools return `{"success": bool, ...}` dicts. Never raise `ToolError`.
- **Tool registration**: Use `@register_tool(category=..., permission_level=..., description=...)` decorator. Tools are auto-discovered via the global `ToolRegistry`.
- **Tool registration**: Use `@register_tool(category=..., capabilities=..., description=...)` decorator. `capabilities=` is required — pass `EXEMPT` for tools that need no permission check or a list of `Capability(name, target_arg=...)` tuples the engine matches against rules. Tools are auto-discovered via the global `ToolRegistry`.
- **Permissions**: `workflow/permissions/` holds a framework-independent engine that evaluates declared capabilities against rules from four sources (builtin, user `~/.{app_name}/settings.json`, project `./.{app_name}/settings.json`, in-memory session). ADK + LangGraph gate tool calls via `workflow/adk/permission_plugin.py::PermissionPlugin` and `workflow/langgraph/permission_wrap.py::wrap_tool_for_permission`. See `docs/superpowers/specs/2026-04-18-permissions-system-design.md`.
- **Service registry**: Tools access services and shared state via `get_service(key)` from `workflow.service_registry`. A single ContextVar holds a `dict[str, Any]` set by the workflow manager during processing. Complex services (KBManager, SandboxManager, MemoryStore) are lazily created; simple state (plan string, task list) lives directly in the registry dict.
- **Manager detection**: Tools decorated with `@requires("kb_manager")` etc. are scanned by `BaseWorkflowManager._detect_required_managers()` which lazily creates only the needed services.
- **Atomic writes**: Use `atomic_write_json`/`atomic_write_text` from `persistence/_utils.py` for file persistence.
Expand Down
49 changes: 26 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Agentic CLI provides the core infrastructure for building interactive CLI applic
├─────────────────────────────┬───────────────────────────────────────┤
│ GoogleADKWorkflowManager │ LangGraphWorkflowManager │
│ (default) │ (optional: langgraph extra) │
│ + ConfirmationPlugin │ + confirmation tool wrapper
│ + PermissionPlugin │ + wrap_tool_for_permission
│ + LLMLoggingPlugin │ + native ToolNode │
│ + TaskProgressPlugin │ │
└─────────────────────────────┴───────────────────────────────────────┘
Expand Down Expand Up @@ -145,7 +145,7 @@ manager = GoogleADKWorkflowManager(
```

ADK integrations:
- **ConfirmationPlugin** — intercepts `DANGEROUS` tools and prompts the user for approval
- **PermissionPlugin** — evaluates each tool's declared capabilities against the permission engine and prompts the user when no rule matches
- **LLMLoggingPlugin** — structured logging of LLM requests/responses
- **TaskProgressPlugin** — streams the task checklist into its own thinking box

Expand All @@ -171,7 +171,7 @@ Features:
- **Explicit provider support**: Uses `langchain-google-genai` for Gemini (not VertexAI)
- **Thinking mode**: Native support for Claude and Gemini thinking/reasoning
- **Retry policies**: Automatic retry with exponential backoff
- **Confirmation wrapper**: Wraps `DANGEROUS` tools to request HITL approval
- **Permission wrapper**: Wraps each tool to gate execution through the permission engine
- **Event streaming**: Real-time workflow events via `WorkflowEvent`

Requires: `pip install agentic-cli[langgraph]`
Expand All @@ -186,7 +186,7 @@ Requires: `pip install agentic-cli[langgraph]`
| State persistence | In-memory | Memory, PostgreSQL, or SQLite |
| Thinking support | Native (Gemini) | Native (Claude & Gemini) |
| Retry handling | Built-in | Built-in with backoff |
| HITL confirmation | ConfirmationPlugin | Tool wrapper |
| Permission gate | PermissionPlugin | wrap_tool_for_permission |
| Context trimming | Native | Native |

### Auto-selection via Settings
Expand Down Expand Up @@ -320,11 +320,12 @@ configs = [coordinator, researcher, analyst]
Tools are regular Python functions with type hints and docstrings. **All tools return `{"success": bool, ...}` dicts** — never raise exceptions.

```python
from agentic_cli.tools import register_tool, ToolCategory, PermissionLevel
from agentic_cli.tools import register_tool, ToolCategory
from agentic_cli.workflow.permissions import Capability, EXEMPT

@register_tool(
category=ToolCategory.DATA,
permission_level=PermissionLevel.SAFE,
category=ToolCategory.NETWORK,
capabilities=[Capability("http.read")],
description="Search the database for matching records.",
)
def search_database(query: str, limit: int = 10) -> dict:
Expand Down Expand Up @@ -534,15 +535,9 @@ Each tool keeps at most N reflections (FIFO eviction). Reflections can be inject

#### HITL (Human-in-the-Loop)

Two mechanisms:
Tool calls are gated by the **permission engine** (`workflow/permissions/`). Each tool declares a list of capabilities (e.g. `filesystem.write(path=...)`); the engine evaluates them against rules from four sources (builtin defaults, user `~/.{app_name}/settings.json`, project `./.{app_name}/settings.json`, in-memory session). When no rule matches, the user is prompted with `Allow once / Allow for session / Allow always (save to project) / Deny`. Always-grants persist into the project settings file so the next run picks them up automatically.

1. **Automatic confirmation for `DANGEROUS` tools** — handled by ADK's `ConfirmationPlugin` and LangGraph's tool wrapper. No code changes needed; just mark the tool `DANGEROUS`.
2. **Explicit `request_approval` tool** — for domain-level checkpoints:

```python
from agentic_cli.tools import hitl_tools
# request_approval(message, options) -> {"success": True, "choice": "..."}
```
See `docs/superpowers/specs/2026-04-18-permissions-system-design.md` for the full design.

## CLI Commands

Expand Down Expand Up @@ -718,15 +713,24 @@ agentic-cli/
│ │ ├── adk/
│ │ │ ├── manager.py # GoogleADKWorkflowManager
│ │ │ ├── event_processor.py
│ │ │ ├── plugins.py # ConfirmationPlugin, LLMLoggingPlugin
│ │ │ ├── plugins.py # LLMLoggingPlugin
│ │ │ ├── permission_plugin.py # PermissionPlugin (capability gating)
│ │ │ └── task_progress_plugin.py
│ │ └── langgraph/
│ │ ├── manager.py # LangGraphWorkflowManager
│ │ ├── graph_builder.py
│ │ ├── state.py
│ │ └── persistence/ # Checkpointers and stores
│ │ ├── langgraph/
│ │ │ ├── manager.py # LangGraphWorkflowManager
│ │ │ ├── graph_builder.py
│ │ │ ├── permission_wrap.py # wrap_tool_for_permission
│ │ │ ├── state.py
│ │ │ └── persistence/ # Checkpointers and stores
│ │ └── permissions/ # Framework-independent permission engine
│ │ ├── capabilities.py # Capability, ResolvedCapability, EXEMPT
│ │ ├── rules.py # Rule, Effect, RuleSource, CheckResult, AskScope
│ │ ├── matchers.py # Path/URL/Shell/StringGlob matchers
│ │ ├── store.py # PermissionContext, BUILTIN_RULES, JSON load/save
│ │ ├── prompt.py # build_request + parse_response
│ │ └── engine.py # PermissionEngine
│ ├── tools/
│ │ ├── registry.py # ToolRegistry, ToolCategory, PermissionLevel
│ │ ├── registry.py # ToolRegistry, ToolCategory, register_tool
│ │ ├── factories.py # Backend-aware tool factories
│ │ ├── executor.py # SafePythonExecutor
│ │ ├── arxiv_tools.py # search_arxiv, fetch_arxiv_paper, ingest_arxiv_paper
Expand All @@ -744,7 +748,6 @@ agentic-cli/
│ │ ├── pdf_utils.py # PDF text extraction helpers
│ │ ├── memory_tools.py # save/search/update/delete + MemoryStore
│ │ ├── reflection_tools.py # save_reflection + ToolReflectionStore
│ │ ├── hitl_tools.py # request_approval
│ │ ├── _core/ # Shared planning/task logic
│ │ │ ├── planning.py
│ │ │ └── tasks.py
Expand Down
5 changes: 0 additions & 5 deletions src/agentic_cli/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

Framework Tools:
- memory_tools: Working and long-term memory tools
- hitl_tools: Human-in-the-loop approval tools
- web_search: Web search with pluggable backends (Tavily, Brave)

For resilience patterns, use tenacity, pybreaker, aiolimiter directly.
Expand Down Expand Up @@ -48,7 +47,6 @@
from agentic_cli.tools.webfetch_tool import web_fetch
from agentic_cli.tools.registry import (
ToolCategory,
PermissionLevel,
ToolDefinition,
ToolRegistry,
get_registry,
Expand All @@ -61,7 +59,6 @@
__all__ = [
# Registry classes
"ToolCategory",
"PermissionLevel",
"ToolDefinition",
"ToolRegistry",
"get_registry",
Expand Down Expand Up @@ -103,7 +100,6 @@
"ask_clarification",
# Framework tool modules (lazy loaded)
"memory_tools",
"hitl_tools",
"sandbox_tools",
"reflection_tools",
]
Expand All @@ -112,7 +108,6 @@
# Lazy loading for framework tool modules
_lazy_tool_modules = {
"memory_tools": "agentic_cli.tools.memory_tools",
"hitl_tools": "agentic_cli.tools.hitl_tools",
"sandbox_tools": "agentic_cli.tools.sandbox",
"reflection_tools": "agentic_cli.tools.reflection_tools",
}
Expand Down
6 changes: 6 additions & 0 deletions src/agentic_cli/tools/adk/state_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@

from agentic_cli.tools._core.planning import summarize_checkboxes
from agentic_cli.tools._core.tasks import validate_tasks, normalize_tasks, filter_tasks
from agentic_cli.tools.registry import ToolCategory, register_tool
from agentic_cli.workflow.permissions import EXEMPT


@register_tool(capabilities=EXEMPT, category=ToolCategory.PLANNING)
def save_plan(content: str, tool_context: ToolContext) -> dict[str, Any]:
"""Save or update the execution plan as markdown with checkboxes.

Expand All @@ -33,6 +36,7 @@ def save_plan(content: str, tool_context: ToolContext) -> dict[str, Any]:
return {"success": True, "message": message}


@register_tool(capabilities=EXEMPT, category=ToolCategory.PLANNING)
def get_plan(tool_context: ToolContext) -> dict[str, Any]:
"""Retrieve the current execution plan.

Expand All @@ -47,6 +51,7 @@ def get_plan(tool_context: ToolContext) -> dict[str, Any]:
return {"success": True, "content": plan}


@register_tool(capabilities=EXEMPT, category=ToolCategory.PLANNING)
def save_tasks(
tasks: list[dict[str, Any]], tool_context: ToolContext
) -> dict[str, Any]:
Expand Down Expand Up @@ -86,6 +91,7 @@ def save_tasks(
}


@register_tool(capabilities=EXEMPT, category=ToolCategory.PLANNING)
def get_tasks(
status: str = "",
priority: str = "",
Expand Down
11 changes: 7 additions & 4 deletions src/agentic_cli/tools/arxiv_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
from agentic_cli.tools.registry import (
register_tool,
ToolCategory,
PermissionLevel,
)
from agentic_cli.workflow.permissions import Capability
from agentic_cli.workflow.service_registry import (
ARXIV_SOURCE,
KB_MANAGER,
Expand Down Expand Up @@ -122,7 +122,8 @@ async def _fetch_arxiv_paper_with_source(source, arxiv_id: str) -> dict[str, Any

@register_tool(
category=ToolCategory.KNOWLEDGE,
permission_level=PermissionLevel.SAFE,

capabilities=[Capability("http.read")],
description="Search arXiv for academic papers by query, category, or date range. Use this to find research papers on a topic.",
)
def search_arxiv(
Expand Down Expand Up @@ -170,7 +171,8 @@ def search_arxiv(

@register_tool(
category=ToolCategory.KNOWLEDGE,
permission_level=PermissionLevel.SAFE,

capabilities=[Capability("http.read")],
description="Fetch metadata for a specific arXiv paper by ID or URL. Returns title, authors, abstract, categories, and PDF URL.",
)
async def fetch_arxiv_paper(
Expand Down Expand Up @@ -296,7 +298,8 @@ async def _ingest_arxiv_paper_with_services(

@register_tool(
category=ToolCategory.KNOWLEDGE,
permission_level=PermissionLevel.SAFE,

capabilities=[Capability("http.read"), Capability("kb.write")],
description="Download an arXiv paper's PDF, extract text, and ingest it into the knowledge base. Use this to add a specific arXiv paper to long-term storage so it can be searched later.",
)
async def ingest_arxiv_paper(
Expand Down
4 changes: 2 additions & 2 deletions src/agentic_cli/tools/execution_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
from agentic_cli.tools.registry import (
register_tool,
ToolCategory,
PermissionLevel,
)
from agentic_cli.workflow.permissions import Capability


@register_tool(
category=ToolCategory.EXECUTION,
permission_level=PermissionLevel.DANGEROUS,
capabilities=[Capability("python.exec")],
description="Stateless Python scratchpad for quick calculations, formula checks, and mathematical reasoning. Each call starts fresh — no state persists. Only whitelisted modules (math, numpy, pandas, json, etc.) are available. Use sandbox_execute instead for stateful work.",
)
def execute_python(
Expand Down
8 changes: 3 additions & 5 deletions src/agentic_cli/tools/file_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
Provides safe, read-only tools for file system access:
- read_file: Read file contents with optional offset/limit
- diff_compare: Compare two text sources

All tools in this module have PermissionLevel.SAFE.
"""

import difflib
Expand All @@ -13,14 +11,14 @@

from agentic_cli.tools.registry import (
ToolCategory,
PermissionLevel,
register_tool,
)
from agentic_cli.workflow.permissions import Capability


@register_tool(
category=ToolCategory.READ,
permission_level=PermissionLevel.SAFE,
capabilities=[Capability("filesystem.read", target_arg="path")],
description="Read the contents of a file at the given path. Use this to examine source code, config files, or any text file. For finding files by name pattern use glob instead; for searching file contents use grep.",
)
def read_file(
Expand Down Expand Up @@ -110,7 +108,7 @@ def read_file(

@register_tool(
category=ToolCategory.READ,
permission_level=PermissionLevel.SAFE,
capabilities=[Capability("filesystem.read", target_arg="source_a"), Capability("filesystem.read", target_arg="source_b")],
description="Compare two text sources (files or strings) and show differences. Use this to see what changed between two versions of content.",
)
def diff_compare(
Expand Down
7 changes: 3 additions & 4 deletions src/agentic_cli/tools/file_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
- write_file: Write content to a file (creates or overwrites)
- edit_file: Replace text in a file (sed-like operation)

All tools in this module have PermissionLevel.CAUTION.
Delete, move, and copy operations should use the shell tool.
"""

Expand All @@ -15,14 +14,14 @@
from agentic_cli.file_utils import atomic_write_text
from agentic_cli.tools.registry import (
ToolCategory,
PermissionLevel,
register_tool,
)
from agentic_cli.workflow.permissions import Capability


@register_tool(
category=ToolCategory.WRITE,
permission_level=PermissionLevel.CAUTION,
capabilities=[Capability("filesystem.write", target_arg="path")],
description="Write content to a file (creates or overwrites). Use this to create new files or replace entire file contents. For partial modifications use edit_file instead.",
)
def write_file(
Expand Down Expand Up @@ -87,7 +86,7 @@ def write_file(

@register_tool(
category=ToolCategory.WRITE,
permission_level=PermissionLevel.CAUTION,
capabilities=[Capability("filesystem.read", target_arg="path"), Capability("filesystem.write", target_arg="path")],
description="Replace specific text in an existing file. Use this for targeted edits (find-and-replace). For creating or fully rewriting files use write_file instead.",
)
def edit_file(
Expand Down
8 changes: 3 additions & 5 deletions src/agentic_cli/tools/glob_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

Provides file discovery using glob patterns:
- glob: Find files matching a pattern (also serves as directory listing)

This is a read-only tool with PermissionLevel.SAFE.
"""

import os
Expand All @@ -13,14 +11,14 @@

from agentic_cli.tools.registry import (
ToolCategory,
PermissionLevel,
register_tool,
)
from agentic_cli.workflow.permissions import Capability


@register_tool(
category=ToolCategory.READ,
permission_level=PermissionLevel.SAFE,
capabilities=[Capability("filesystem.read", target_arg="path")],
description="Find files by name pattern (e.g. '**/*.py'). Use this to discover files in a directory tree. For searching inside file contents, use grep instead.",
)
def glob(
Expand Down Expand Up @@ -137,7 +135,7 @@ def glob(

@register_tool(
category=ToolCategory.READ,
permission_level=PermissionLevel.SAFE,
capabilities=[Capability("filesystem.read", target_arg="path")],
description="List directory contents organized by type (directories first, then files). Use this when you need a structured overview of a directory; for pattern-based file search use glob.",
)
def list_dir(
Expand Down
6 changes: 2 additions & 4 deletions src/agentic_cli/tools/grep_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

Provides pattern-based content search across files:
- grep: Search for patterns in files (ripgrep-like interface)

This is a read-only tool with PermissionLevel.SAFE.
"""

import functools
Expand All @@ -14,14 +12,14 @@

from agentic_cli.tools.registry import (
ToolCategory,
PermissionLevel,
register_tool,
)
from agentic_cli.workflow.permissions import Capability


@register_tool(
category=ToolCategory.READ,
permission_level=PermissionLevel.SAFE,
capabilities=[Capability("filesystem.read", target_arg="path")],
description="Search for text patterns inside file contents (regex or literal). Use this to find code references, function definitions, or any text across files. For finding files by name, use glob instead.",
)
def grep(
Expand Down
Loading