connectonion 0.6.3__py3-none-any.whl → 0.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
connectonion/__init__.py CHANGED
@@ -10,7 +10,7 @@ LLM-Note:
10
10
  ConnectOnion - A simple agent framework with behavior tracking.
11
11
  """
12
12
 
13
- __version__ = "0.6.3"
13
+ __version__ = "0.6.4"
14
14
 
15
15
  # Auto-load .env files for the entire framework
16
16
  from dotenv import load_dotenv
@@ -12,9 +12,9 @@ from .tools import (
12
12
  load_guide,
13
13
  )
14
14
  from .skills import skill
15
- from .plugins import reminder_plugin
15
+ from .plugins import system_reminder
16
16
  from connectonion import Agent, bash, after_user_input, FileWriter, MODE_AUTO, MODE_NORMAL, TodoList
17
- from connectonion.useful_plugins import re_act, eval
17
+ from connectonion.useful_plugins import eval, tool_approval
18
18
 
19
19
 
20
20
  PROMPTS_DIR = Path(__file__).parent / "prompts"
@@ -71,7 +71,7 @@ def create_coding_agent(
71
71
  if project_context:
72
72
  system_prompt += f"\n\n---\n\n{project_context}"
73
73
 
74
- plugins = [re_act, eval, reminder_plugin]
74
+ plugins = [eval, system_reminder, tool_approval]
75
75
 
76
76
  agent = Agent(
77
77
  name="oo",
@@ -1,6 +1,5 @@
1
1
  """OO Agent plugins."""
2
2
 
3
- from .shell_approval import shell_approval
4
- from .reminder import reminder_plugin
3
+ from .system_reminder import system_reminder
5
4
 
6
- __all__ = ['shell_approval', 'reminder_plugin']
5
+ __all__ = ['system_reminder']
@@ -0,0 +1,154 @@
1
+ """
2
+ System Reminder Plugin - Injects contextual guidance based on intent and tool usage.
3
+
4
+ Two triggers:
5
+ 1. after_user_input: Detect intent (coding, agent creation) and inject relevant reminder
6
+ 2. after_each_tool: Inject reminder based on tool usage
7
+
8
+ Usage:
9
+ from connectonion.cli.co_ai.plugins.system_reminder import system_reminder
10
+
11
+ agent = Agent("coder", plugins=[system_reminder])
12
+ """
13
+
14
+ from pathlib import Path
15
+ import fnmatch
16
+ from typing import TYPE_CHECKING
17
+
18
+ from connectonion.core.events import after_each_tool, after_user_input
19
+ from connectonion.llm_do import llm_do
20
+
21
+ if TYPE_CHECKING:
22
+ from connectonion.core.agent import Agent
23
+
24
+ # Default reminders directory
25
+ REMINDERS_DIR = Path(__file__).parent.parent / "prompts" / "system-reminders"
26
+
27
+ # Intent detection prompt
28
+ INTENT_PROMPT = """Analyze the user's request.
29
+
30
+ User request: {user_prompt}
31
+
32
+ Is this about building software, creating agents, writing code, or automation?
33
+ Respond with ONE word only: "build" or "other"
34
+
35
+ One word only:"""
36
+
37
+
38
+ def _parse_frontmatter(text):
39
+ """Parse YAML frontmatter from markdown."""
40
+ if not text.startswith('---'):
41
+ return {}, text
42
+ parts = text.split('---', 2)
43
+ if len(parts) < 3:
44
+ return {}, text
45
+ import yaml
46
+ return yaml.safe_load(parts[1]) or {}, parts[2].strip()
47
+
48
+
49
+ def _load_reminders(reminders_dir):
50
+ """Load all .md reminder files from directory."""
51
+ reminders_dir = Path(reminders_dir)
52
+ if not reminders_dir.exists():
53
+ return {}
54
+ reminders = {}
55
+ for f in reminders_dir.glob("*.md"):
56
+ meta, body = _parse_frontmatter(f.read_text())
57
+ if meta.get('name'):
58
+ reminders[meta['name']] = {
59
+ 'content': body,
60
+ 'triggers': meta.get('triggers', []),
61
+ 'intent': meta.get('intent'), # New: intent-based trigger
62
+ }
63
+ return reminders
64
+
65
+
66
+ def _matches_pattern(pattern, value):
67
+ """Check if value matches glob pattern(s)."""
68
+ if not pattern or not value:
69
+ return False
70
+ patterns = [pattern] if isinstance(pattern, str) else pattern
71
+ return any(fnmatch.fnmatch(value, p) for p in patterns)
72
+
73
+
74
+ def _find_tool_reminder(reminders, tool_name, args):
75
+ """Find matching reminder for tool usage."""
76
+ for reminder in reminders.values():
77
+ for trigger in reminder['triggers']:
78
+ if trigger.get('tool') and trigger['tool'] != tool_name:
79
+ continue
80
+ if trigger.get('path_pattern'):
81
+ path = args.get('path') or args.get('file_path', '')
82
+ if not _matches_pattern(trigger['path_pattern'], path):
83
+ continue
84
+ if trigger.get('command_pattern'):
85
+ cmd = args.get('command') or args.get('cmd', '')
86
+ if not _matches_pattern(trigger['command_pattern'], cmd):
87
+ continue
88
+ # All conditions matched
89
+ content = reminder['content']
90
+ path = args.get('path') or args.get('file_path', '')
91
+ return content.replace('${file_path}', path).replace('${tool_name}', tool_name)
92
+ return None
93
+
94
+
95
+ def _find_intent_reminder(reminders, intent):
96
+ """Find matching reminder for detected intent."""
97
+ for reminder in reminders.values():
98
+ if reminder.get('intent') == intent:
99
+ return reminder['content']
100
+ return None
101
+
102
+
103
+ # Load reminders once at import
104
+ _REMINDERS = _load_reminders(REMINDERS_DIR)
105
+
106
+
107
+ @after_user_input
108
+ def detect_intent(agent: 'Agent') -> None:
109
+ """Detect user intent and inject relevant system reminder."""
110
+ user_prompt = agent.current_session.get('user_prompt', '')
111
+ if not user_prompt:
112
+ return
113
+
114
+ # Use llm_do to detect intent
115
+ intent = llm_do(
116
+ INTENT_PROMPT.format(user_prompt=user_prompt),
117
+ model="co/gemini-2.5-flash",
118
+ temperature=0,
119
+ ).strip().lower()
120
+
121
+ # Store intent in session
122
+ agent.current_session['intent'] = intent
123
+
124
+ # Find and inject intent-based reminder
125
+ content = _find_intent_reminder(_REMINDERS, intent)
126
+ if content:
127
+ agent.current_session['messages'].append({
128
+ 'role': 'user',
129
+ 'content': f"\n\n{content}"
130
+ })
131
+
132
+
133
+ @after_each_tool
134
+ def inject_tool_reminder(agent: 'Agent') -> None:
135
+ """Inject matching system reminder into tool result."""
136
+ trace = agent.current_session.get('trace', [])
137
+ messages = agent.current_session.get('messages', [])
138
+ if not trace or not messages:
139
+ return
140
+
141
+ last = trace[-1]
142
+ if last.get('type') != 'tool_result':
143
+ return
144
+
145
+ content = _find_tool_reminder(_REMINDERS, last.get('name', ''), last.get('args', {}))
146
+ if content:
147
+ for msg in reversed(messages):
148
+ if msg.get('role') == 'tool':
149
+ msg['content'] = msg.get('content', '') + '\n\n' + content
150
+ break
151
+
152
+
153
+ # Export plugin
154
+ system_reminder = [detect_intent, inject_tool_reminder]
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: build
3
+ intent: build
4
+ ---
5
+
6
+ <system-reminder>
7
+ Agent creation detected. Use ConnectOnion framework:
8
+
9
+ **ConnectOnion agents are Python files (.py), NOT markdown files.**
10
+
11
+ ```python
12
+ from connectonion import Agent
13
+
14
+ def list_files(dir: str) -> list[str]: ...
15
+ def get_hash(path: str) -> str: ...
16
+ def delete(path: str) -> str: ...
17
+
18
+ agent = Agent("cleaner", tools=[list_files, get_hash, delete])
19
+ agent.input("Remove duplicate files")
20
+ ```
21
+
22
+ **Give tools, not logic. Let the agent decide strategy. NEVER create standalone scripts with hardcoded logic.**
23
+ </system-reminder>
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: plan-mode
3
+ triggers:
4
+ - tool: enter_plan_mode
5
+ ---
6
+
7
+ <system-reminder>
8
+ Plan mode is active. You MUST NOT make any edits or run non-readonly tools.
9
+
10
+ Only allowed: glob, grep, read_file, write_plan, exit_plan_mode
11
+
12
+ Write your plan to the plan file, then call exit_plan_mode when done.
13
+ </system-reminder>
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: security
3
+ triggers:
4
+ - tool: read_file
5
+ path_pattern: ["*.env*", "*credentials*", "*secrets*", "*password*", "*token*", "*.pem", "*.key"]
6
+ - tool: read
7
+ path_pattern: ["*.env*", "*credentials*", "*secrets*", "*password*", "*token*", "*.pem", "*.key"]
8
+ ---
9
+
10
+ <system-reminder>
11
+ This file may contain sensitive information.
12
+ - Never expose secrets in output
13
+ - Never commit real credentials
14
+ </system-reminder>
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: simplicity
3
+ triggers:
4
+ - tool: edit
5
+ - tool: multi_edit
6
+ - tool: write
7
+ ---
8
+
9
+ <system-reminder>
10
+ Keep it simple:
11
+ - Only change what's directly needed
12
+ - Don't add error handling for scenarios that can't happen
13
+ - Three similar lines > premature abstraction
14
+ </system-reminder>
@@ -6,8 +6,6 @@ from rich.console import Console
6
6
  from rich.panel import Panel
7
7
  from rich.markdown import Markdown
8
8
 
9
- from connectonion.cli.co_ai.reminders import REMINDERS
10
-
11
9
  console = Console()
12
10
 
13
11
  # Plan mode state (module-level for simplicity)
@@ -101,8 +99,7 @@ def enter_plan_mode() -> str:
101
99
  border_style="green"
102
100
  ))
103
101
 
104
- plan_mode_reminder = REMINDERS.get("plan_mode_active", "")
105
- return f"Entered plan mode. Write your plan to {_plan_file_path}, then call exit_plan_mode() when ready for user approval.\n\n{plan_mode_reminder}"
102
+ return f"Entered plan mode. Write your plan to {_plan_file_path}, then call exit_plan_mode() when ready for user approval."
106
103
 
107
104
 
108
105
  def exit_plan_mode() -> str:
@@ -3,8 +3,6 @@
3
3
  from pathlib import Path
4
4
  from typing import Optional
5
5
 
6
- from connectonion.cli.co_ai.reminders import inject_reminder, should_show_security_reminder
7
-
8
6
 
9
7
  def read_file(
10
8
  path: str,
@@ -60,8 +58,4 @@ def read_file(
60
58
  if end < total_lines:
61
59
  result += f"\n\n... ({total_lines - end} more lines)"
62
60
 
63
- # Inject security reminder for sensitive files
64
- if should_show_security_reminder(path):
65
- result = inject_reminder(result, "security")
66
-
67
61
  return result
@@ -54,9 +54,10 @@ agent = Agent("a", plugins=[re_act, logger])
54
54
  | `eval` | Task evaluation for debugging | [eval.md](../useful_plugins/eval.md) |
55
55
  | `image_result_formatter` | Format images for vision models | [image_result_formatter.md](../useful_plugins/image_result_formatter.md) |
56
56
  | `shell_approval` | Approve shell commands before execution | [shell_approval.md](../useful_plugins/shell_approval.md) |
57
+ | `tool_approval` | Web-based approval for dangerous tools | [tool_approval.md](../useful_plugins/tool_approval.md) |
57
58
 
58
59
  ```python
59
- from connectonion.useful_plugins import re_act, eval, image_result_formatter, shell_approval
60
+ from connectonion.useful_plugins import re_act, eval, image_result_formatter, shell_approval, tool_approval
60
61
 
61
62
  # Combine plugins
62
63
  agent = Agent("assistant", plugins=[re_act, image_result_formatter])
@@ -0,0 +1,139 @@
1
+ # tool_approval
2
+
3
+ Web-based approval for dangerous tools via WebSocket. Requires user confirmation before executing tools that can modify files or run commands.
4
+
5
+ ## Quick Start
6
+
7
+ ```python
8
+ from connectonion import Agent, bash
9
+ from connectonion.useful_plugins import tool_approval
10
+
11
+ agent = Agent("assistant", tools=[bash], plugins=[tool_approval])
12
+ agent.io = my_websocket_io # Required for web mode
13
+
14
+ agent.input("Install dependencies")
15
+ # → Client receives: {"type": "approval_needed", "tool": "bash", "arguments": {"command": "npm install"}}
16
+ # → Client responds: {"approved": true, "scope": "session"}
17
+ # ✓ bash approved (session)
18
+ ```
19
+
20
+ ## How It Works
21
+
22
+ 1. Before each tool executes, check if it's dangerous
23
+ 2. If dangerous, send `approval_needed` event via WebSocket
24
+ 3. Wait for client response (blocks until received)
25
+ 4. If approved: execute tool, optionally remember for session
26
+ 5. If rejected: stop batch, return feedback to LLM
27
+
28
+ ## Tool Classification
29
+
30
+ ### Safe Tools (No Approval)
31
+
32
+ Read-only operations that never modify state:
33
+
34
+ ```
35
+ read, read_file, glob, grep, search
36
+ list_files, get_file_info, task, load_guide
37
+ enter_plan_mode, exit_plan_mode, write_plan
38
+ task_output, ask_user
39
+ ```
40
+
41
+ ### Dangerous Tools (Require Approval)
42
+
43
+ Operations that can modify files or have side effects:
44
+
45
+ ```
46
+ bash, shell, run, run_in_dir
47
+ write, edit, multi_edit
48
+ run_background, kill_task
49
+ send_email, post, delete, remove
50
+ ```
51
+
52
+ ## Client Protocol
53
+
54
+ ### Receive from server
55
+
56
+ ```json
57
+ {
58
+ "type": "approval_needed",
59
+ "tool": "bash",
60
+ "arguments": {"command": "npm install"}
61
+ }
62
+ ```
63
+
64
+ ### Send response
65
+
66
+ ```json
67
+ // Approve for this session (no re-prompting)
68
+ {"approved": true, "scope": "session"}
69
+
70
+ // Approve once only
71
+ {"approved": true, "scope": "once"}
72
+
73
+ // Reject with feedback
74
+ {"approved": false, "feedback": "Use yarn instead"}
75
+ ```
76
+
77
+ ## Approval Scopes
78
+
79
+ | Scope | Behavior |
80
+ |-------|----------|
81
+ | `once` | Approve this call only |
82
+ | `session` | Approve for rest of session (stored in memory) |
83
+
84
+ ## Rejection Behavior
85
+
86
+ When user rejects a tool:
87
+
88
+ 1. Raises `ValueError` with feedback message
89
+ 2. Stops the entire tool batch (remaining tools skipped)
90
+ 3. LLM receives the error and can adjust approach
91
+
92
+ ```python
93
+ # Example error message
94
+ "User rejected tool 'bash'. Feedback: Use yarn instead"
95
+ ```
96
+
97
+ ## Terminal Logging
98
+
99
+ The plugin logs all approval decisions:
100
+
101
+ ```
102
+ ✓ bash approved (session) # Approved with session scope
103
+ ✓ edit approved (once) # Approved for single use
104
+ ⏭ bash (session-approved) # Skipped (already approved)
105
+ ✗ bash rejected: Use yarn # Rejected with feedback
106
+ ✗ bash - connection closed # WebSocket closed
107
+ ```
108
+
109
+ ## Events
110
+
111
+ | Handler | Event | Purpose |
112
+ |---------|-------|---------|
113
+ | `check_approval` | `before_each_tool` | Check approval and prompt client |
114
+
115
+ ## Session Data
116
+
117
+ ```python
118
+ # Approval state stored in session
119
+ agent.current_session['approval'] = {
120
+ 'approved_tools': {
121
+ 'bash': 'session', # Approved for session
122
+ 'write': 'session' # Approved for session
123
+ }
124
+ }
125
+ ```
126
+
127
+ ## Non-Web Mode
128
+
129
+ When `agent.io` is None (not web mode), all tools execute without approval. This is the default behavior for CLI usage.
130
+
131
+ ## Unknown Tools
132
+
133
+ Tools not in SAFE_TOOLS or DANGEROUS_TOOLS are treated as safe and execute without approval.
134
+
135
+ ## See Also
136
+
137
+ - [shell_approval](shell_approval.md) - Terminal-based approval for shell commands
138
+ - [Events](../concepts/events.md) - Available event hooks
139
+ - [Plugins](../concepts/plugins.md) - Plugin system overview
@@ -18,5 +18,6 @@ from .gmail_plugin import gmail_plugin
18
18
  from .calendar_plugin import calendar_plugin
19
19
  from .ui_stream import ui_stream
20
20
  from .system_reminder import system_reminder
21
+ from .tool_approval import tool_approval
21
22
 
22
- __all__ = ['re_act', 'eval', 'image_result_formatter', 'shell_approval', 'gmail_plugin', 'calendar_plugin', 'ui_stream', 'system_reminder']
23
+ __all__ = ['re_act', 'eval', 'image_result_formatter', 'shell_approval', 'gmail_plugin', 'calendar_plugin', 'ui_stream', 'system_reminder', 'tool_approval']
@@ -0,0 +1,233 @@
1
+ """
2
+ Purpose: Web-based tool approval plugin - request user approval before dangerous tools
3
+ LLM-Note:
4
+ Dependencies: imports from [core/events.py] | imported by [useful_plugins/__init__.py] | tested by [tests/unit/test_tool_approval.py]
5
+ Data flow: before_each_tool fires → check if dangerous tool → io.send(approval_needed) → io.receive() blocks → approved: continue, rejected: raise ValueError
6
+ State/Effects: stores approved_tools in session for "session" scope approvals | blocks on io.receive() until client responds | logs all approval decisions via agent.logger
7
+ Integration: exposes tool_approval plugin list | uses agent.io for WebSocket communication | requires client to handle "approval_needed" events
8
+ Errors: raises ValueError on rejection (stops batch, feedback sent to LLM)
9
+
10
+ Tool Approval Plugin - Request client approval before executing dangerous tools.
11
+
12
+ WebSocket-only. Uses io.send/receive pattern:
13
+ 1. Sends {type: "approval_needed", tool, arguments} to client
14
+ 2. Blocks until client responds with {approved: bool, scope?, feedback?}
15
+ 3. If approved: execute tool (optionally save to session memory)
16
+ 4. If rejected: raise ValueError, stopping batch, LLM sees feedback
17
+
18
+ Tool Classification:
19
+ - SAFE_TOOLS: Read-only operations (read, glob, grep, etc.) - never need approval
20
+ - DANGEROUS_TOOLS: Write/execute operations (bash, write, edit, etc.) - always need approval
21
+ - Unknown tools: Treated as safe (no approval needed)
22
+
23
+ Session Memory:
24
+ - scope="once": Approve for this call only
25
+ - scope="session": Approve for rest of session (no re-prompting)
26
+
27
+ Rejection Behavior:
28
+ - Raises ValueError with user feedback
29
+ - Stops entire tool batch (remaining tools skipped)
30
+ - LLM receives error message and can adjust approach
31
+
32
+ Usage:
33
+ from connectonion import Agent
34
+ from connectonion.useful_plugins import tool_approval
35
+
36
+ agent = Agent("assistant", tools=[bash, write], plugins=[tool_approval])
37
+
38
+ Client Protocol:
39
+ # Receive from server:
40
+ {"type": "approval_needed", "tool": "bash", "arguments": {"command": "npm install"}}
41
+
42
+ # Send response:
43
+ {"approved": true, "scope": "session"} # Approve for session
44
+ {"approved": true, "scope": "once"} # Approve once
45
+ {"approved": false, "feedback": "Use yarn instead"} # Reject with feedback
46
+ """
47
+
48
+ from typing import TYPE_CHECKING
49
+
50
+ from ..core.events import before_each_tool
51
+
52
+ if TYPE_CHECKING:
53
+ from ..core.agent import Agent
54
+
55
+
56
+ # Tools that NEVER need approval (read-only, safe)
57
+ # These tools cannot modify system state or have external side effects.
58
+ # Add new read-only tools here to skip approval prompts.
59
+ SAFE_TOOLS = {
60
+ # File reading - read contents without modification
61
+ 'read', 'read_file',
62
+ # Search operations - find files/content without modification
63
+ 'glob', 'grep', 'search',
64
+ # Info operations - query metadata only
65
+ 'list_files', 'get_file_info',
66
+ # Agent operations - sub-agents handle their own approval
67
+ 'task',
68
+ # Documentation - load reference materials
69
+ 'load_guide',
70
+ # Planning - state management without side effects
71
+ 'enter_plan_mode', 'exit_plan_mode', 'write_plan',
72
+ # Task management - read-only task status
73
+ 'task_output',
74
+ # User interaction - prompts user, not system modification
75
+ 'ask_user',
76
+ }
77
+
78
+ # Tools that ALWAYS need approval (destructive/side-effects)
79
+ # These tools can modify files, execute code, or have external effects.
80
+ # User approval required before execution in web mode.
81
+ DANGEROUS_TOOLS = {
82
+ # Shell execution - arbitrary command execution
83
+ 'bash', 'shell', 'run', 'run_in_dir',
84
+ # File modification - write/edit file contents
85
+ 'write', 'edit', 'multi_edit',
86
+ # Background tasks - long-running command execution
87
+ 'run_background',
88
+ # Task control - terminate running processes
89
+ 'kill_task',
90
+ # External communication - send data outside system
91
+ 'send_email', 'post',
92
+ # Deletion - remove files/resources
93
+ 'delete', 'remove',
94
+ }
95
+
96
+
97
+ # Session state helpers for approval memory
98
+ # These functions manage the session['approval'] dict which tracks
99
+ # which tools have been approved for the current session.
100
+
101
+ def _init_approval_state(session: dict) -> None:
102
+ """Initialize approval state in session if not present.
103
+
104
+ Creates session['approval']['approved_tools'] dict for storing
105
+ tool approvals with scope='session'.
106
+ """
107
+ if 'approval' not in session:
108
+ session['approval'] = {
109
+ 'approved_tools': {}, # tool_name -> 'session'
110
+ }
111
+
112
+
113
+ def _is_approved_for_session(session: dict, tool_name: str) -> bool:
114
+ """Check if tool was approved for this session.
115
+
116
+ Returns True if user previously approved this tool with scope='session'.
117
+ """
118
+ approval = session.get('approval', {})
119
+ return approval.get('approved_tools', {}).get(tool_name) == 'session'
120
+
121
+
122
+ def _save_session_approval(session: dict, tool_name: str) -> None:
123
+ """Save tool as approved for this session.
124
+
125
+ Future calls to the same tool will skip approval prompts.
126
+ """
127
+ _init_approval_state(session)
128
+ session['approval']['approved_tools'][tool_name] = 'session'
129
+
130
+
131
+ def _log(agent: 'Agent', message: str, style: str = None) -> None:
132
+ """Log message via agent's logger if available.
133
+
134
+ Args:
135
+ agent: Agent instance
136
+ message: Message to log
137
+ style: Rich style string (e.g., "[green]", "[red]")
138
+ """
139
+ if hasattr(agent, 'logger') and agent.logger:
140
+ agent.logger.print(message, style)
141
+
142
+
143
+ @before_each_tool
144
+ def check_approval(agent: 'Agent') -> None:
145
+ """Check if tool needs approval and request from client.
146
+
147
+ Flow:
148
+ 1. Skip if no IO (not web mode)
149
+ 2. Skip if safe tool
150
+ 3. Skip if unknown tool (default: safe)
151
+ 4. Skip if already approved for session
152
+ 5. Send approval_needed, wait for response
153
+ 6. If approved: optionally save to session, continue
154
+ 7. If rejected: raise ValueError (stops batch)
155
+
156
+ Logging:
157
+ - Logs approval requests, approvals, and rejections
158
+ - Uses agent.logger.print() for terminal output
159
+
160
+ Raises:
161
+ ValueError: If user rejects the tool (includes feedback if provided)
162
+ """
163
+ # No IO = not web mode, skip
164
+ if not agent.io:
165
+ return
166
+
167
+ # Get pending tool info
168
+ pending = agent.current_session.get('pending_tool')
169
+ if not pending:
170
+ return
171
+
172
+ tool_name = pending['name']
173
+ tool_args = pending['arguments']
174
+
175
+ # Safe tools don't need approval
176
+ if tool_name in SAFE_TOOLS:
177
+ return
178
+
179
+ # Unknown tools (not in SAFE or DANGEROUS) are treated as safe
180
+ if tool_name not in DANGEROUS_TOOLS:
181
+ return
182
+
183
+ # Already approved for this session
184
+ if _is_approved_for_session(agent.current_session, tool_name):
185
+ _log(agent, f"[dim]⏭ {tool_name} (session-approved)[/dim]")
186
+ return
187
+
188
+ # Send approval request to client
189
+ agent.io.send({
190
+ 'type': 'approval_needed',
191
+ 'tool': tool_name,
192
+ 'arguments': tool_args,
193
+ })
194
+
195
+ # Wait for client response (BLOCKS)
196
+ response = agent.io.receive()
197
+
198
+ # Handle connection closed
199
+ if response.get('type') == 'io_closed':
200
+ _log(agent, f"[red]✗ {tool_name} - connection closed[/red]")
201
+ raise ValueError(f"Connection closed while waiting for approval of '{tool_name}'")
202
+
203
+ # Check approval
204
+ approved = response.get('approved', False)
205
+
206
+ if approved:
207
+ # Save to session if scope is "session"
208
+ scope = response.get('scope', 'once')
209
+ if scope == 'session':
210
+ _save_session_approval(agent.current_session, tool_name)
211
+ _log(agent, f"[green]✓ {tool_name} approved (session)[/green]")
212
+ else:
213
+ _log(agent, f"[green]✓ {tool_name} approved (once)[/green]")
214
+ # Continue to execute tool
215
+ return
216
+
217
+ # Rejected - raise ValueError to stop batch
218
+ feedback = response.get('feedback', '')
219
+ if feedback:
220
+ _log(agent, f"[red]✗ {tool_name} rejected: {feedback}[/red]")
221
+ else:
222
+ _log(agent, f"[red]✗ {tool_name} rejected[/red]")
223
+
224
+ error_msg = f"User rejected tool '{tool_name}'."
225
+ if feedback:
226
+ error_msg += f" Feedback: {feedback}"
227
+ raise ValueError(error_msg)
228
+
229
+
230
+ # Export as plugin (list of event handlers)
231
+ # Usage: Agent("name", plugins=[tool_approval])
232
+ # The plugin registers check_approval as a before_each_tool handler
233
+ tool_approval = [check_approval]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: connectonion
3
- Version: 0.6.3
3
+ Version: 0.6.4
4
4
  Summary: A simple Python framework for creating AI agents with behavior tracking
5
5
  Project-URL: Homepage, https://github.com/openonion/connectonion
6
6
  Project-URL: Documentation, https://docs.connectonion.com
@@ -1,4 +1,4 @@
1
- connectonion/__init__.py,sha256=xlQNWUE80CDHRNIVvY3apw36sMkQoCwIyZ1ay4msG_8,3416
1
+ connectonion/__init__.py,sha256=rlIzfNx6-3ijsVefkkZHtF_3oqRHmDekaEDoJXBkoLY,3416
2
2
  connectonion/address.py,sha256=YOzpMOej-HqJUE6o0i0fG8rB7HM-Iods36s9OD--5ig,10852
3
3
  connectonion/console.py,sha256=Gl0K0c3ZHlLkbGlBVx0Wgb5Fg8LNVci9WQhSDDdGmJg,21937
4
4
  connectonion/llm_do.py,sha256=rwgSsTreNGAq5xV3m9lbA7U5AE0XOZNdihJwW5FHz0k,12005
@@ -19,10 +19,9 @@ connectonion/cli/browser_agent/prompts/form_filler.md,sha256=r4Trnln51rjKTIYGJ9S
19
19
  connectonion/cli/browser_agent/prompts/scroll_strategy.md,sha256=fbvEtMM4J9yhGXNeDdKCf4D5ZB5fA-KQrLapmul0wVU,833
20
20
  connectonion/cli/browser_agent/scripts/extract_elements.js,sha256=0YMufRSeBf6PQLxbpgVmHvlnjPVNTYqmmsuWyOZBGNc,4651
21
21
  connectonion/cli/co_ai/__init__.py,sha256=rxHdQFxV3iH9y60VhuoKu_jly02JHbdQEHAkpMZPrIM,183
22
- connectonion/cli/co_ai/agent.py,sha256=Oc8DJ4iFO9zZlFxsRX3Sp_EEAwlmaihw1QUP5grQWO4,2160
22
+ connectonion/cli/co_ai/agent.py,sha256=4xVunKfK9s61TOrn0nDpFn2_j627fRi9K_EJlLtx5Rg,2174
23
23
  connectonion/cli/co_ai/context.py,sha256=B07GMFUFqAP-_76PxJigHgj7AMkbMBX0kAObysCz5tM,3778
24
24
  connectonion/cli/co_ai/main.py,sha256=k9SvRrs2nFLBOWCrqjULfxWKwpcRxwiqvDkNxWEwvtA,1480
25
- connectonion/cli/co_ai/reminders.py,sha256=x5C14uFQWEP6L1T15-zzS5jVLCoIDfRJGsS2lc0Oo7Q,4898
26
25
  connectonion/cli/co_ai/sessions.py,sha256=YbkTowHw2BpSIh2sGg0avnJLuwObkKeFZ0UXGkXpLlg,3526
27
26
  connectonion/cli/co_ai/agents/__init__.py,sha256=k9KdUsmsG80Us4_SnJ4m9Ibx0KNVQ_ly0JlNFjcX1gc,142
28
27
  connectonion/cli/co_ai/agents/registry.py,sha256=-hhgh6S9iufwKoxT-kO3mhN-wLYSlB_YJVxoK3I4598,1740
@@ -35,9 +34,8 @@ connectonion/cli/co_ai/commands/init.py,sha256=w_SoRNU8CwULlIvWkRwxNcItb9XnSWKw0
35
34
  connectonion/cli/co_ai/commands/sessions.py,sha256=4F-s86wgd_xbSshz2G4O7Kfgzz61fwv3JLmYjuJbonE,1667
36
35
  connectonion/cli/co_ai/commands/tasks.py,sha256=BoxyrFSAb4uKAiZRAu1u3f0UjepCD-Es3jSDQlENFA4,1767
37
36
  connectonion/cli/co_ai/commands/undo.py,sha256=WE_TA4Td0uSBaHZcyW_kxYI59nmpFcrrSSDVCRq8BJg,2736
38
- connectonion/cli/co_ai/plugins/__init__.py,sha256=a46RJBGocjOfrpW5oUc3_kaornkQw7IVGFDIQ3cxefE,155
39
- connectonion/cli/co_ai/plugins/reminder.py,sha256=xZvZw8hEjlC5SLvgzx4ztmKGs9IFaXSN9ojsE1Xxia0,2611
40
- connectonion/cli/co_ai/plugins/shell_approval.py,sha256=uolVMC6JCJGTcyaDq8UvQ_m-OaCCYZlagU4cckAqkW0,4097
37
+ connectonion/cli/co_ai/plugins/__init__.py,sha256=vC3R3b8JDJ9KICFlRsOPmE3XTpnvOmuz5t-XW8icMI8,101
38
+ connectonion/cli/co_ai/plugins/system_reminder.py,sha256=XSVmljS4_cOKzayOrBcv9JtNx03I40u4ZD8mw9Zj1SM,4889
41
39
  connectonion/cli/co_ai/prompts/assembler.py,sha256=ubhS2cT68z0PqdXdjlzIRVodX3o7iTvg0XCu0vHUDxM,9742
42
40
  connectonion/cli/co_ai/prompts/main.md,sha256=Dq5F32HLpFwrM8jKB53hWX_Ztw4qggRxRtFausnsZE8,8591
43
41
  connectonion/cli/co_ai/prompts/summarization.md,sha256=Qu5T8qWU7ZtLUSgJb_Ga1_vYt7on9_dp1MnFMCyg41c,1979
@@ -153,7 +151,10 @@ connectonion/cli/co_ai/prompts/connectonion/useful_tools/slash_command.md,sha256
153
151
  connectonion/cli/co_ai/prompts/connectonion/useful_tools/terminal.md,sha256=BdPz0cnsD8JkMLs5zhSPlKQbhV0vIXWbDfrxnkT12IM,1751
154
152
  connectonion/cli/co_ai/prompts/connectonion/useful_tools/todo_list.md,sha256=8qQrdtlNnGip1oocDYEGMItomOffFKHZ6xGXYDgN6-Y,5266
155
153
  connectonion/cli/co_ai/prompts/connectonion/useful_tools/web_fetch.md,sha256=uMnlsMHbUfh2M1Vnaf8aaMnveMTuatnpd92r2sn-uIo,2241
156
- connectonion/cli/co_ai/prompts/reminders/plan_mode.md,sha256=sgepJBZ_4sKlN3JeOa8VlZatjDqSDv3lNaZcgEF5k5c,1685
154
+ connectonion/cli/co_ai/prompts/system-reminders/agent.md,sha256=4Yifx5W1kuQVZ8qA3Kwpf-bwi_F-zCaf6jjMJoT1FwA,569
155
+ connectonion/cli/co_ai/prompts/system-reminders/plan_mode.md,sha256=Qh5P7LapxAw9NBYjRWtgBNgJ2sTepSErL85ruCQXiIA,310
156
+ connectonion/cli/co_ai/prompts/system-reminders/security.md,sha256=WlLWakeEUR_w9ZOVg8AiHuGX4QO-BaB45AJ06tgcPbc,420
157
+ connectonion/cli/co_ai/prompts/system-reminders/simplicity.md,sha256=kKqz_Op9A6Dh35UHHxiUdEY2En63Z-R2tNrYwLi--KY,283
157
158
  connectonion/cli/co_ai/prompts/tools/ask_user.md,sha256=H3YwaxdXy9uxxPIcTlbOp9wjl-0ZXnIRuvrTOzIKpdM,1458
158
159
  connectonion/cli/co_ai/prompts/tools/background.md,sha256=ipJFKe9NM61EvL0QoNYpzAbmRwsSrV2Q_C2puj9ZKsE,1400
159
160
  connectonion/cli/co_ai/prompts/tools/edit.md,sha256=2cxl-IID6tItdeASQxgcKtoRLVvYWNLafhfGjIFW_Rc,2500
@@ -179,8 +180,8 @@ connectonion/cli/co_ai/tools/glob.py,sha256=GrX7ozs61mkf8uG4E5Pi3w8aPP8QYrLnC5Pj
179
180
  connectonion/cli/co_ai/tools/grep.py,sha256=5VXMbFE-uY_JVAXTOevpTTzeB8oXw7XsdawM5TLqN2s,5063
180
181
  connectonion/cli/co_ai/tools/load_guide.py,sha256=tAomLc21O_UJIieRdX4L1oUTOMcZBIO4XoDyhcMe1PA,617
181
182
  connectonion/cli/co_ai/tools/multi_edit.py,sha256=BNmkWJRhuM-W4WW-c8lVFo3zbIIvoadjfeunD_nO7xY,4423
182
- connectonion/cli/co_ai/tools/plan_mode.py,sha256=pPi4k0ZjetFBu4SRiJvP0HDKEkxSaRGLX0ZJFa6hZMg,5053
183
- connectonion/cli/co_ai/tools/read.py,sha256=_iHMJS9-kS8PAEGdlE1HhG-yFzPawVf20-BAKkFYTWg,1892
183
+ connectonion/cli/co_ai/tools/plan_mode.py,sha256=enyynOtXb7hS4xZZaNuSJAMZ22oXCrH0sItFdyBp1v8,4910
184
+ connectonion/cli/co_ai/tools/read.py,sha256=-I-5i01m7fgmQBXlfhGUyW61SLszaVJpNarGU9Z2qrw,1650
184
185
  connectonion/cli/co_ai/tools/task.py,sha256=-65KJl4tdo0l-aeRIL-AXHKCKMBG9x6z2gAau-AfUHE,1817
185
186
  connectonion/cli/co_ai/tools/todo_list.py,sha256=t2uQICQELM_dDCJcD2A0basxbMYV9bnX_nDbqQlKahk,4983
186
187
  connectonion/cli/co_ai/tools/write.py,sha256=nSO-ASVZXy4acj40QZ3afVYHSYaFaL-OcqPdYQaQlbw,3577
@@ -277,7 +278,7 @@ connectonion/tui/providers.py,sha256=7e3PXQv6xtasSSne6PEUAz8yEG430uN9FAafioALbEo
277
278
  connectonion/tui/status_bar.py,sha256=MUJICFKp4fm2Hres2mt5dbVsw1OwssbuI-KuzwY1fZA,5479
278
279
  connectonion/useful_events_handlers/__init__.py,sha256=V2iLrD_ryP6ubIKmN3HwsU-9OI-O1y76m747u92-RWc,826
279
280
  connectonion/useful_events_handlers/reflect.py,sha256=z6BGx7JuzhG0AXc0XtJn83YG3KVxecQAwMQcSyfbRbs,4653
280
- connectonion/useful_plugins/__init__.py,sha256=PqQQ5NTnKnEa_1G1kh6xn7oBrDKHQZ8vVhl9p4k0mQQ,1419
281
+ connectonion/useful_plugins/__init__.py,sha256=8uiZ1jpNzoHjePaQQEKF4KJ1u8jLiHyN1Kik-_gyft0,1477
281
282
  connectonion/useful_plugins/calendar_plugin.py,sha256=PoQoOfLcprDDBRrt1Ykzlh2RDiOofIyL7tO-hERkYV8,6004
282
283
  connectonion/useful_plugins/eval.py,sha256=6uJn2mZZiJpMQ1e-6Nw042wdYSioFrRMHA-MZZeT388,4932
283
284
  connectonion/useful_plugins/gmail_plugin.py,sha256=94H31zWOjwAuiDMdgg5tnlu-1I9yxkM0ZU42w2YdJOI,5702
@@ -285,6 +286,7 @@ connectonion/useful_plugins/image_result_formatter.py,sha256=sYcyn3L3YvC24Mnu3Mk
285
286
  connectonion/useful_plugins/re_act.py,sha256=q1l99zGtKmK6SGxLFGeaCM3v274fXfWOHwHoXLgZKVY,5828
286
287
  connectonion/useful_plugins/shell_approval.py,sha256=ytmwXpgvjX0VLABsY7XyTpKiB_68uA2MUzS9vVwLJws,6361
287
288
  connectonion/useful_plugins/system_reminder.py,sha256=eLeOyGDyNH_TBlQblRg5CJ3XLhh0jaWrGPSD0hjZ1Ag,3338
289
+ connectonion/useful_plugins/tool_approval.py,sha256=P403MLpQBdDO7Loz3wVlXtAhVGNaj7frMB5NKOdDuyc,8334
288
290
  connectonion/useful_plugins/ui_stream.py,sha256=Jsh5URgIGBLJAmLnRne7XikRoI0x2TePt0DrhU7yYWA,2152
289
291
  connectonion/useful_prompts/README.md,sha256=jMIjzxjyu5zc1Mk8KAUvZlN9iroiznckbOn9_sLjMBo,1636
290
292
  connectonion/useful_prompts/__init__.py,sha256=ps5sON_kafzx9nt8KqLQF0QDoFSOYAhrlxFshS5zXlI,1795
@@ -375,7 +377,7 @@ connectonion/docs/concepts/events.md,sha256=AwVddkW7ZqkFWr1ie8Z5GfSUE1xzyCF3j_Y6
375
377
  connectonion/docs/concepts/llm_do.md,sha256=1Ns_k77RAAGBRvz7Z9_5c4IsEgSUYx3_EthSewV9dvI,6759
376
378
  connectonion/docs/concepts/max_iterations.md,sha256=EUEMjQi2Lvv8Ab_maONIo3hM4_0SU1W7p22E87tN1uc,10782
377
379
  connectonion/docs/concepts/models.md,sha256=rohjOX_eT2cu1jsvQxIOpVDAvX6tinqHMCfgZCGt6zY,20113
378
- connectonion/docs/concepts/plugins.md,sha256=NHKHV6cKrujnTFv7V6Lx_5LZXiuUuoc-ncfGve73l6s,2651
380
+ connectonion/docs/concepts/plugins.md,sha256=13MjQF3QObY0Avn4lQg6-2ZarzBE3MN-n9Z-61Ng-zY,2784
379
381
  connectonion/docs/concepts/prompts.md,sha256=LXiyNSniGrD0PZeEkadmzR9Fc7qVR8hj01KZCIklSyU,3191
380
382
  connectonion/docs/concepts/session.md,sha256=7wPqsvm3DDNuV2yBg1LqdHe83ZCL5qR2julWc8-jCxQ,10573
381
383
  connectonion/docs/concepts/tools.md,sha256=2y8Bi31UqQHmDFf8dbhm7vYweWcPCl4ebYkicGFKJuU,14518
@@ -446,6 +448,7 @@ connectonion/docs/useful_plugins/image_result_formatter.md,sha256=FucmJjccl9MuY5
446
448
  connectonion/docs/useful_plugins/re_act.md,sha256=QWC6kB6R2foOpLZuO0Hll4FhPDdxtM82kWYenRkuMj8,2345
447
449
  connectonion/docs/useful_plugins/shell_approval.md,sha256=OA4cZZRB8ueJQrjrLvk_4W2LBI1FP16tfJBI8DGqf8U,1776
448
450
  connectonion/docs/useful_plugins/system_reminder.md,sha256=Hm-vxTD5A2fgoodufSdmkVSDZecP-na5ylFBZAXRvCI,5509
451
+ connectonion/docs/useful_plugins/tool_approval.md,sha256=fiZbrXTH1UOXH95Yfrjc1OWJ5HNbylOiLXP-sF_6amo,3482
449
452
  connectonion/docs/useful_prompts/README.md,sha256=Wln15T2FMLCMFqXAU7g4se2lQVSQcCAV2CLrK7sJDNM,3062
450
453
  connectonion/docs/useful_prompts/coding_agent.md,sha256=1MQLjKKzRANUWE5hc5csvOgHHwLzSY_bfQRlB-w6dEE,4571
451
454
  connectonion/docs/useful_tools/README.md,sha256=v1HZRzkeXCLmNwlDIGQK6dZEXJQ2WV11U9LoLMD_5Xg,2572
@@ -463,7 +466,7 @@ connectonion/docs/useful_tools/slash_command.md,sha256=B4jTn9Bck19rVdecZCUqMSUdc
463
466
  connectonion/docs/useful_tools/terminal.md,sha256=SeAt2BNN_91dVuxIHpFbT9V_i8XPTCR4-U2UX1_05RU,2041
464
467
  connectonion/docs/useful_tools/todo_list.md,sha256=4MGHUYYamYXV_NfYi179IvmKuAdxBW14nD1g4PrLjl4,5537
465
468
  connectonion/docs/useful_tools/web_fetch.md,sha256=uri7ZjhJ8rFgy3SNolzGMlXTHGEBCIUHWlwpXKWqEDw,2516
466
- connectonion-0.6.3.dist-info/METADATA,sha256=ItPOfKQtRjL4szH6yrJRVYIhB-HTMJrbvTrdG5EtoBY,22190
467
- connectonion-0.6.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
468
- connectonion-0.6.3.dist-info/entry_points.txt,sha256=XDB-kVN7Qgy4DmYTkjQB_O6hZeUND-SqmZbdoQPn6WA,90
469
- connectonion-0.6.3.dist-info/RECORD,,
469
+ connectonion-0.6.4.dist-info/METADATA,sha256=x8-IEs4md_WjbpnMM1q6sGv4AnO_0H6vPARWuGjq_To,22190
470
+ connectonion-0.6.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
471
+ connectonion-0.6.4.dist-info/entry_points.txt,sha256=XDB-kVN7Qgy4DmYTkjQB_O6hZeUND-SqmZbdoQPn6WA,90
472
+ connectonion-0.6.4.dist-info/RECORD,,
@@ -1,76 +0,0 @@
1
- """
2
- Reminder plugin - injects contextual reminders into tool results.
3
-
4
- Like Claude Code's system reminders, these are appended to tool results
5
- (not separate messages) to guide agent behavior without extra API calls.
6
-
7
- Usage:
8
- from connectonion.cli.co_ai.plugins.reminder import reminder_plugin
9
-
10
- agent = Agent("coder", plugins=[reminder_plugin])
11
- """
12
-
13
- from connectonion.core.events import after_each_tool
14
- from ..reminders import REMINDERS, should_show_security_reminder
15
-
16
-
17
- def _get_reminder_for_tool(tool_name: str, args: dict, result: str) -> str | None:
18
- """Determine which reminder to inject based on tool and context."""
19
-
20
- # write_file with .py extension → remind about ConnectOnion pattern
21
- if tool_name == "write_file":
22
- path = args.get("path", "") or args.get("file_path", "")
23
- if path.endswith(".py"):
24
- return "connectonion_workflow"
25
-
26
- # read_file with sensitive path → security reminder
27
- if tool_name in ("read_file", "read"):
28
- path = args.get("path", "") or args.get("file_path", "")
29
- if should_show_security_reminder(path):
30
- return "security"
31
-
32
- # bash/shell commands that modify code
33
- if tool_name in ("bash", "shell", "run_command"):
34
- cmd = args.get("command", "") or args.get("cmd", "")
35
- # If creating/editing Python files
36
- if any(x in cmd for x in [">.py", ">> .py", "cat >", "echo >", "sed -i"]):
37
- return "connectonion_workflow"
38
-
39
- return None
40
-
41
-
42
- def inject_reminder_handler(agent):
43
- """Inject contextual reminders into tool results.
44
-
45
- This handler runs after each tool execution and modifies the
46
- tool result message to include relevant reminders.
47
- """
48
- trace = agent.current_session.get('trace', [])
49
- messages = agent.current_session.get('messages', [])
50
-
51
- if not trace or not messages:
52
- return
53
-
54
- # Get the most recent tool execution
55
- last_trace = trace[-1]
56
- if last_trace.get('type') != 'tool_result':
57
- return
58
-
59
- tool_name = last_trace.get('name', '')
60
- tool_args = last_trace.get('args', {})
61
- result = last_trace.get('result', '')
62
-
63
- # Determine which reminder to inject
64
- reminder_key = _get_reminder_for_tool(tool_name, tool_args, result)
65
- if not reminder_key or reminder_key not in REMINDERS:
66
- return
67
-
68
- # Find and modify the last tool result message
69
- for msg in reversed(messages):
70
- if msg.get('role') == 'tool':
71
- msg['content'] = msg.get('content', '') + '\n\n' + REMINDERS[reminder_key]
72
- break
73
-
74
-
75
- # Export the plugin
76
- reminder_plugin = [after_each_tool(inject_reminder_handler)]
@@ -1,105 +0,0 @@
1
- """Shell Approval plugin - Asks user approval for shell commands."""
2
-
3
- import re
4
- from typing import TYPE_CHECKING
5
- from connectonion.core.events import before_each_tool
6
-
7
- if TYPE_CHECKING:
8
- from connectonion.core.agent import Agent
9
-
10
- SAFE_PATTERNS = [
11
- r'^ls\b', r'^ll\b', r'^cat\b', r'^head\b', r'^tail\b', r'^less\b', r'^more\b',
12
- r'^grep\b', r'^rg\b', r'^find\b', r'^fd\b', r'^which\b', r'^whereis\b',
13
- r'^type\b', r'^file\b', r'^stat\b', r'^wc\b', r'^pwd\b', r'^echo\b',
14
- r'^printf\b', r'^date\b', r'^whoami\b', r'^id\b', r'^env\b', r'^printenv\b',
15
- r'^uname\b', r'^hostname\b', r'^df\b', r'^du\b', r'^free\b', r'^ps\b',
16
- r'^top\b', r'^htop\b', r'^tree\b',
17
- r'^git\s+status\b', r'^git\s+log\b', r'^git\s+diff\b', r'^git\s+show\b',
18
- r'^git\s+branch\b', r'^git\s+remote\b', r'^git\s+tag\b',
19
- r'^npm\s+list\b', r'^npm\s+ls\b', r'^pip\s+list\b', r'^pip\s+show\b',
20
- r'^python\s+--version\b', r'^node\s+--version\b', r'^cargo\s+--version\b',
21
- ]
22
-
23
-
24
- def _is_safe(command: str) -> bool:
25
- cmd = command.strip()
26
- return any(re.search(pattern, cmd) for pattern in SAFE_PATTERNS)
27
-
28
-
29
- def _check_approval(agent: 'Agent') -> None:
30
- pending = agent.current_session.get('pending_tool') if agent.current_session else None
31
- if not pending:
32
- return
33
-
34
- tool_name = pending.get('name', '')
35
- if tool_name not in ('bash', 'shell', 'run', 'run_in_dir'):
36
- return
37
-
38
- args = pending.get('arguments', {})
39
- command = args.get('command', '')
40
- base_cmd = command.strip().split()[0] if command.strip() else ''
41
-
42
- approved_cmds = agent.current_session.get('shell_approved_cmds', set()) if agent.current_session else set()
43
- if base_cmd in approved_cmds:
44
- return
45
-
46
- if _is_safe(command):
47
- return
48
-
49
- from connectonion.cli.co_ai.tui.context import is_tui_active, show_choice_selector_sync, show_modal_sync
50
-
51
- if is_tui_active():
52
- from connectonion.cli.co_ai.tui.modals import TextInputModal
53
-
54
- truncated = command[:60] + "..." if len(command) > 60 else command
55
- question = f"Execute: `{truncated}`"
56
- options = [
57
- "Yes, execute",
58
- f"Auto approve '{base_cmd}' for this session",
59
- "No, tell agent what I want",
60
- ]
61
-
62
- choice = show_choice_selector_sync(question, options, allow_other=False)
63
-
64
- if choice == options[0]:
65
- return
66
- elif choice == options[1]:
67
- if agent.current_session is not None:
68
- if 'shell_approved_cmds' not in agent.current_session:
69
- agent.current_session['shell_approved_cmds'] = set()
70
- agent.current_session['shell_approved_cmds'].add(base_cmd)
71
- return
72
- else:
73
- feedback = show_modal_sync(TextInputModal("What do you want instead?"))
74
- raise ValueError(f"User feedback: {feedback}")
75
- else:
76
- from rich.console import Console
77
- from rich.panel import Panel
78
- from rich.syntax import Syntax
79
- from connectonion.tui import pick
80
-
81
- console = Console()
82
- console.print()
83
- syntax = Syntax(command, "bash", theme="monokai", word_wrap=True)
84
- console.print(Panel(syntax, title="[yellow]Shell Command[/yellow]", border_style="yellow"))
85
-
86
- choice = pick("Execute this command?", [
87
- "Yes, execute",
88
- f"Auto approve '{base_cmd}' in this session",
89
- "No, tell agent what I want"
90
- ], console=console)
91
-
92
- if choice == "Yes, execute":
93
- return
94
- elif choice.startswith("Auto approve"):
95
- if agent.current_session is not None:
96
- if 'shell_approved_cmds' not in agent.current_session:
97
- agent.current_session['shell_approved_cmds'] = set()
98
- agent.current_session['shell_approved_cmds'].add(base_cmd)
99
- return
100
- else:
101
- feedback = input("What do you want the agent to do instead? ")
102
- raise ValueError(f"User feedback: {feedback}")
103
-
104
-
105
- shell_approval = [before_each_tool(_check_approval)]
@@ -1,34 +0,0 @@
1
- Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools, or otherwise make any changes to the system. This supersedes any other instructions you have received.
2
-
3
- ## Plan File Info
4
- ${PLAN_EXISTS ? "A plan file already exists at ${PLAN_FILE_PATH}. You can read it and make incremental edits." : "No plan file exists yet. You should create your plan at ${PLAN_FILE_PATH}."}
5
-
6
- You should build your plan incrementally by writing to or editing this file. This is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
7
-
8
- ## Plan Workflow
9
-
10
- ### Phase 1: Initial Understanding
11
- Goal: Understand the user's request by reading through code and asking questions.
12
-
13
- 1. Focus on understanding the user's request and the code associated with it
14
- 2. Use exploration tools to understand the codebase structure
15
- 3. Ask clarifying questions to resolve ambiguities
16
-
17
- ### Phase 2: Design
18
- Goal: Design an implementation approach based on your exploration.
19
-
20
- 1. Consider different approaches and their trade-offs
21
- 2. Identify the files that need to be modified
22
- 3. Plan the order of changes
23
-
24
- ### Phase 3: Final Plan
25
- Goal: Write your final plan to the plan file.
26
-
27
- - Include only your recommended approach, not all alternatives
28
- - Be concise enough to scan quickly, but detailed enough to execute
29
- - Include the paths of critical files to be modified
30
-
31
- ### Phase 4: Exit Plan Mode
32
- Once you are happy with your final plan, call the exit_plan_mode tool to indicate you are done planning.
33
-
34
- NOTE: Feel free to ask the user questions at any point. Don't make large assumptions about user intent.
@@ -1,159 +0,0 @@
1
- """System reminders for contextual guidance.
2
-
3
- System reminders are automatically injected into tool results or conversation
4
- to provide contextual constraints and guidance. They override default behavior
5
- when applicable.
6
-
7
- Usage:
8
- from connectonion.cli.co_ai.reminders import inject_reminder, REMINDERS
9
-
10
- # Inject a specific reminder
11
- result = inject_reminder(tool_result, "plan_mode_active")
12
-
13
- # Check if reminder should be shown
14
- if should_show_todo_reminder(agent):
15
- result = inject_reminder(result, "todo_reminder")
16
- """
17
-
18
- from typing import Optional, Dict, Any
19
- from functools import wraps
20
-
21
-
22
- # System reminder templates
23
- REMINDERS: Dict[str, str] = {
24
- # Plan mode reminder - injected when plan mode is active
25
- "plan_mode_active": """<system-reminder>
26
- Plan mode is active. You are in READ-ONLY exploration mode.
27
- - You can ONLY use: glob, grep, read_file to explore
28
- - You can ONLY write to the plan file (.co/PLAN.md)
29
- - Do NOT make any code changes until plan is approved
30
- - Use write_plan() to update your plan
31
- - Use exit_plan_mode() when plan is complete
32
- </system-reminder>""",
33
-
34
- # Todo reminder - gentle nudge to use todo tracking
35
- "todo_reminder": """<system-reminder>
36
- Consider using todo_list() to track progress for multi-step tasks.
37
- Mark tasks complete immediately when finished.
38
- </system-reminder>""",
39
-
40
- # Read-only mode for explore agent
41
- "read_only": """<system-reminder>
42
- This is READ-ONLY mode. You are PROHIBITED from modifying any files.
43
- Only use: glob, grep, read_file, and read-only bash commands.
44
- </system-reminder>""",
45
-
46
- # Security reminder after reading sensitive files
47
- "security": """<system-reminder>
48
- This file may contain sensitive information (credentials, keys, tokens).
49
- - Never expose secrets in output
50
- - Never commit this file if it contains real credentials
51
- - Consider using environment variables instead
52
- </system-reminder>""",
53
-
54
- # Anti-over-engineering reminder
55
- "simplicity": """<system-reminder>
56
- Keep it simple:
57
- - Only change what's directly needed
58
- - Don't add error handling for scenarios that can't happen
59
- - Three similar lines > premature abstraction
60
- - If unused, delete completely
61
- </system-reminder>""",
62
-
63
- # Workflow reminder - injected when writing Python files
64
- "connectonion_workflow": """<system-reminder>
65
- When creating agents, ALWAYS use ConnectOnion framework:
66
- - `from connectonion import Agent`
67
- - Atomic tool functions (one thing each)
68
- - `agent = Agent("name", tools=[...])` + `agent.input("task")`
69
-
70
- NEVER create standalone scripts with argparse. NEVER skip ask_user confirmation.
71
- </system-reminder>""",
72
-
73
- # After code write reminder
74
- "after_write_code": """<system-reminder>
75
- Code written. If this is an agent:
76
- - Verify it uses `from connectonion import Agent`
77
- - Verify tools are atomic functions
78
- - If it's a standalone script with argparse, REWRITE using ConnectOnion
79
- </system-reminder>""",
80
- }
81
-
82
-
83
- def inject_reminder(content: str, reminder_key: str) -> str:
84
- """
85
- Inject a system reminder into content.
86
-
87
- Args:
88
- content: The original content (tool result, message, etc.)
89
- reminder_key: Key from REMINDERS dict
90
-
91
- Returns:
92
- Content with reminder injected at the end
93
- """
94
- if reminder_key not in REMINDERS:
95
- return content
96
-
97
- reminder = REMINDERS[reminder_key]
98
- return f"{content}\n\n{reminder}"
99
-
100
-
101
- def with_reminder(reminder_key: str):
102
- """
103
- Decorator to inject a reminder into tool results.
104
-
105
- Usage:
106
- @with_reminder("plan_mode_active")
107
- def some_tool(...):
108
- return result
109
- """
110
- def decorator(func):
111
- @wraps(func)
112
- def wrapper(*args, **kwargs):
113
- result = func(*args, **kwargs)
114
- if isinstance(result, str):
115
- return inject_reminder(result, reminder_key)
116
- return result
117
- return wrapper
118
- return decorator
119
-
120
-
121
- def should_show_security_reminder(file_path: str) -> bool:
122
- """Check if file path suggests sensitive content."""
123
- sensitive_patterns = [
124
- ".env",
125
- "credentials",
126
- "secrets",
127
- "config/prod",
128
- "keys",
129
- "password",
130
- "token",
131
- ".pem",
132
- ".key",
133
- ]
134
- path_lower = file_path.lower()
135
- return any(pattern in path_lower for pattern in sensitive_patterns)
136
-
137
-
138
- def get_contextual_reminders(context: Dict[str, Any]) -> list:
139
- """
140
- Get list of reminders based on current context.
141
-
142
- Args:
143
- context: Dict with current state info:
144
- - plan_mode: bool
145
- - todo_count: int
146
- - file_path: str (for security check)
147
-
148
- Returns:
149
- List of reminder keys that should be shown
150
- """
151
- reminders = []
152
-
153
- if context.get("plan_mode"):
154
- reminders.append("plan_mode_active")
155
-
156
- if context.get("file_path") and should_show_security_reminder(context["file_path"]):
157
- reminders.append("security")
158
-
159
- return reminders