connectonion 0.6.3__py3-none-any.whl → 0.6.5__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 +1 -1
- connectonion/cli/co_ai/agent.py +3 -3
- connectonion/cli/co_ai/main.py +2 -2
- connectonion/cli/co_ai/plugins/__init__.py +2 -3
- connectonion/cli/co_ai/plugins/system_reminder.py +154 -0
- connectonion/cli/co_ai/prompts/connectonion/concepts/trust.md +166 -208
- connectonion/cli/co_ai/prompts/system-reminders/agent.md +23 -0
- connectonion/cli/co_ai/prompts/system-reminders/plan_mode.md +13 -0
- connectonion/cli/co_ai/prompts/system-reminders/security.md +14 -0
- connectonion/cli/co_ai/prompts/system-reminders/simplicity.md +14 -0
- connectonion/cli/co_ai/tools/plan_mode.py +1 -4
- connectonion/cli/co_ai/tools/read.py +0 -6
- connectonion/cli/commands/copy_commands.py +21 -0
- connectonion/cli/commands/trust_commands.py +152 -0
- connectonion/cli/main.py +82 -0
- connectonion/core/llm.py +2 -2
- connectonion/docs/concepts/fast_rules.md +237 -0
- connectonion/docs/concepts/onboarding.md +465 -0
- connectonion/docs/concepts/plugins.md +2 -1
- connectonion/docs/concepts/trust.md +933 -192
- connectonion/docs/design-decisions/023-trust-policy-system-design.md +323 -0
- connectonion/docs/network/README.md +23 -1
- connectonion/docs/network/connect.md +135 -0
- connectonion/docs/network/host.md +73 -4
- connectonion/docs/useful_plugins/tool_approval.md +139 -0
- connectonion/network/__init__.py +7 -6
- connectonion/network/asgi/__init__.py +3 -0
- connectonion/network/asgi/http.py +125 -19
- connectonion/network/asgi/websocket.py +276 -15
- connectonion/network/connect.py +145 -29
- connectonion/network/host/auth.py +70 -67
- connectonion/network/host/routes.py +88 -3
- connectonion/network/host/server.py +100 -17
- connectonion/network/trust/__init__.py +27 -19
- connectonion/network/trust/factory.py +51 -24
- connectonion/network/trust/fast_rules.py +100 -0
- connectonion/network/trust/tools.py +316 -32
- connectonion/network/trust/trust_agent.py +403 -0
- connectonion/transcribe.py +1 -1
- connectonion/useful_plugins/__init__.py +2 -1
- connectonion/useful_plugins/tool_approval.py +233 -0
- {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/METADATA +1 -1
- {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/RECORD +45 -37
- connectonion/cli/co_ai/plugins/reminder.py +0 -76
- connectonion/cli/co_ai/plugins/shell_approval.py +0 -105
- connectonion/cli/co_ai/prompts/reminders/plan_mode.md +0 -34
- connectonion/cli/co_ai/reminders.py +0 -159
- connectonion/network/trust/prompts.py +0 -71
- {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/WHEEL +0 -0
- {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
connectonion/network/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ LLM-Note:
|
|
|
4
4
|
Dependencies: imports from [host/, io/, connect.py, relay.py, announce.py, trust/] | imported by [__init__.py main package, user code] | tested via submodule tests
|
|
5
5
|
Data flow: pure re-export module aggregating networking functionality
|
|
6
6
|
State/Effects: no state
|
|
7
|
-
Integration: exposes host(agent, port, trust), create_app(), IO/WebSocketIO, SessionStorage/Session, connect(url), RemoteAgent, Response, relay server (relay_connect, serve_loop), announce (create_announce_message), trust (
|
|
7
|
+
Integration: exposes host(agent, port, trust), create_app(), IO/WebSocketIO, SessionStorage/Session, connect(url), RemoteAgent, Response, relay server (relay_connect, serve_loop), announce (create_announce_message), trust (TrustAgent) | unified networking API surface
|
|
8
8
|
Performance: trivial
|
|
9
9
|
Errors: none
|
|
10
10
|
Network layer for hosting and connecting agents.
|
|
@@ -16,7 +16,7 @@ This module contains:
|
|
|
16
16
|
- relay: Agent relay server for P2P discovery
|
|
17
17
|
- connect: Multi-agent networking (RemoteAgent)
|
|
18
18
|
- announce: Service announcement protocol
|
|
19
|
-
- trust: Trust verification system
|
|
19
|
+
- trust: Trust verification system (TrustAgent is the single interface)
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
from .host import host, create_app, SessionStorage, Session
|
|
@@ -24,7 +24,7 @@ from .io import IO, WebSocketIO
|
|
|
24
24
|
from .connect import connect, RemoteAgent, Response
|
|
25
25
|
from .relay import connect as relay_connect, serve_loop
|
|
26
26
|
from .announce import create_announce_message
|
|
27
|
-
from .trust import
|
|
27
|
+
from .trust import TrustAgent, Decision, get_default_trust_level, TRUST_LEVELS, parse_policy
|
|
28
28
|
from . import relay, announce
|
|
29
29
|
|
|
30
30
|
__all__ = [
|
|
@@ -40,11 +40,12 @@ __all__ = [
|
|
|
40
40
|
"relay_connect",
|
|
41
41
|
"serve_loop",
|
|
42
42
|
"create_announce_message",
|
|
43
|
-
|
|
43
|
+
# Trust (TrustAgent is the single interface)
|
|
44
|
+
"TrustAgent",
|
|
45
|
+
"Decision",
|
|
44
46
|
"get_default_trust_level",
|
|
45
47
|
"TRUST_LEVELS",
|
|
46
|
-
"
|
|
47
|
-
"TRUST_PROMPTS",
|
|
48
|
+
"parse_policy",
|
|
48
49
|
"relay",
|
|
49
50
|
"announce",
|
|
50
51
|
]
|
|
@@ -23,6 +23,7 @@ def create_app(
|
|
|
23
23
|
route_handlers: dict,
|
|
24
24
|
storage,
|
|
25
25
|
trust: str = "careful",
|
|
26
|
+
trust_config: dict | None = None,
|
|
26
27
|
blacklist: list | None = None,
|
|
27
28
|
whitelist: list | None = None,
|
|
28
29
|
):
|
|
@@ -32,6 +33,7 @@ def create_app(
|
|
|
32
33
|
route_handlers: Dict of route handler functions
|
|
33
34
|
storage: SessionStorage instance
|
|
34
35
|
trust: Trust level (open/careful/strict)
|
|
36
|
+
trust_config: Parsed YAML config from trust policy (for /info onboard)
|
|
35
37
|
blacklist: Blocked identities
|
|
36
38
|
whitelist: Allowed identities
|
|
37
39
|
|
|
@@ -49,6 +51,7 @@ def create_app(
|
|
|
49
51
|
route_handlers=route_handlers,
|
|
50
52
|
storage=storage,
|
|
51
53
|
trust=trust,
|
|
54
|
+
trust_config=trust_config,
|
|
52
55
|
start_time=start_time,
|
|
53
56
|
blacklist=blacklist,
|
|
54
57
|
whitelist=whitelist,
|
|
@@ -107,6 +107,7 @@ async def handle_http(
|
|
|
107
107
|
route_handlers: dict,
|
|
108
108
|
storage,
|
|
109
109
|
trust: str,
|
|
110
|
+
trust_config: dict | None = None,
|
|
110
111
|
start_time: float,
|
|
111
112
|
blacklist: list | None = None,
|
|
112
113
|
whitelist: list | None = None,
|
|
@@ -120,6 +121,7 @@ async def handle_http(
|
|
|
120
121
|
route_handlers: Dict of route handler functions (input, session, sessions, health, info, auth)
|
|
121
122
|
storage: SessionStorage instance
|
|
122
123
|
trust: Trust level (open/careful/strict)
|
|
124
|
+
trust_config: Parsed YAML config from trust policy (for /info onboard)
|
|
123
125
|
start_time: Server start time
|
|
124
126
|
blacklist: Blocked identities
|
|
125
127
|
whitelist: Allowed identities
|
|
@@ -133,26 +135,130 @@ async def handle_http(
|
|
|
133
135
|
await send({"type": "http.response.body", "body": b""})
|
|
134
136
|
return
|
|
135
137
|
|
|
136
|
-
# Admin endpoints
|
|
138
|
+
# Admin endpoints
|
|
137
139
|
if path.startswith("/admin"):
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if "
|
|
148
|
-
|
|
140
|
+
# Legacy admin endpoints (Bearer token auth)
|
|
141
|
+
if path in ["/admin/logs", "/admin/sessions"]:
|
|
142
|
+
headers_dict = dict(scope.get("headers", []))
|
|
143
|
+
auth = headers_dict.get(b"authorization", b"").decode()
|
|
144
|
+
expected = os.environ.get("OPENONION_API_KEY", "")
|
|
145
|
+
if not expected or not auth.startswith("Bearer ") or not hmac.compare_digest(auth[7:], expected):
|
|
146
|
+
await send_json(send, {"error": "unauthorized"}, 401)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
if method == "GET" and path == "/admin/logs":
|
|
150
|
+
result = route_handlers["admin_logs"]()
|
|
151
|
+
if "error" in result:
|
|
152
|
+
await send_json(send, result, 404)
|
|
153
|
+
else:
|
|
154
|
+
await send_text(send, result["content"])
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
if method == "GET" and path == "/admin/sessions":
|
|
158
|
+
await send_json(send, route_handlers["admin_sessions"]())
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
# Admin trust routes (signed request + admin check)
|
|
162
|
+
if path.startswith("/admin/trust/") or path.startswith("/superadmin/"):
|
|
163
|
+
trust_agent = route_handlers["trust_agent"]
|
|
164
|
+
|
|
165
|
+
# For GET requests, allow auth via headers (bodies often stripped by proxies)
|
|
166
|
+
headers_dict = dict(scope.get("headers", []))
|
|
167
|
+
header_from = headers_dict.get(b"x-from", b"").decode()
|
|
168
|
+
header_sig = headers_dict.get(b"x-signature", b"").decode()
|
|
169
|
+
header_ts = headers_dict.get(b"x-timestamp", b"").decode()
|
|
170
|
+
|
|
171
|
+
if method == "GET" and header_from and header_sig and header_ts:
|
|
172
|
+
# Build signed request from headers
|
|
173
|
+
try:
|
|
174
|
+
data = {
|
|
175
|
+
"payload": {"timestamp": float(header_ts)},
|
|
176
|
+
"from": header_from,
|
|
177
|
+
"signature": header_sig
|
|
178
|
+
}
|
|
179
|
+
except ValueError:
|
|
180
|
+
await send_json(send, {"error": "Invalid X-Timestamp header"}, 400)
|
|
181
|
+
return
|
|
149
182
|
else:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
183
|
+
# Read from body (POST or GET without headers)
|
|
184
|
+
body = await read_body(receive)
|
|
185
|
+
try:
|
|
186
|
+
data = json.loads(body) if body else {}
|
|
187
|
+
except json.JSONDecodeError:
|
|
188
|
+
await send_json(send, {"error": "Invalid JSON"}, 400)
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# Authenticate signed request (reuse same auth as /input, but with "open" trust)
|
|
192
|
+
_, identity, sig_valid, err = route_handlers["auth"](data, "open")
|
|
193
|
+
if err or not sig_valid:
|
|
194
|
+
await send_json(send, {"error": err or "unauthorized: invalid signature"}, 401)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# Check admin permission
|
|
198
|
+
if path.startswith("/superadmin/"):
|
|
199
|
+
# Super admin only (self address)
|
|
200
|
+
if not trust_agent.is_super_admin(identity):
|
|
201
|
+
await send_json(send, {"error": "forbidden: super admin only"}, 403)
|
|
202
|
+
return
|
|
203
|
+
else:
|
|
204
|
+
# Any admin
|
|
205
|
+
if not trust_agent.is_admin(identity):
|
|
206
|
+
await send_json(send, {"error": "forbidden: admin only"}, 403)
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
payload = data.get("payload", {})
|
|
210
|
+
client_id = payload.get("client_id")
|
|
211
|
+
|
|
212
|
+
# Route to handlers
|
|
213
|
+
if method == "POST" and path == "/admin/trust/promote":
|
|
214
|
+
if not client_id:
|
|
215
|
+
await send_json(send, {"error": "client_id required"}, 400)
|
|
216
|
+
return
|
|
217
|
+
await send_json(send, route_handlers["admin_trust_promote"](client_id))
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
if method == "POST" and path == "/admin/trust/demote":
|
|
221
|
+
if not client_id:
|
|
222
|
+
await send_json(send, {"error": "client_id required"}, 400)
|
|
223
|
+
return
|
|
224
|
+
await send_json(send, route_handlers["admin_trust_demote"](client_id))
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
if method == "POST" and path == "/admin/trust/block":
|
|
228
|
+
if not client_id:
|
|
229
|
+
await send_json(send, {"error": "client_id required"}, 400)
|
|
230
|
+
return
|
|
231
|
+
reason = payload.get("reason", "")
|
|
232
|
+
await send_json(send, route_handlers["admin_trust_block"](client_id, reason))
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
if method == "POST" and path == "/admin/trust/unblock":
|
|
236
|
+
if not client_id:
|
|
237
|
+
await send_json(send, {"error": "client_id required"}, 400)
|
|
238
|
+
return
|
|
239
|
+
await send_json(send, route_handlers["admin_trust_unblock"](client_id))
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
if method == "GET" and path.startswith("/admin/trust/level/"):
|
|
243
|
+
client_id = path[len("/admin/trust/level/"):]
|
|
244
|
+
await send_json(send, route_handlers["admin_trust_level"](client_id))
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
if method == "POST" and path == "/superadmin/add":
|
|
248
|
+
admin_id = payload.get("admin_id")
|
|
249
|
+
if not admin_id:
|
|
250
|
+
await send_json(send, {"error": "admin_id required"}, 400)
|
|
251
|
+
return
|
|
252
|
+
await send_json(send, route_handlers["admin_admins_add"](admin_id))
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
if method == "POST" and path == "/superadmin/remove":
|
|
256
|
+
admin_id = payload.get("admin_id")
|
|
257
|
+
if not admin_id:
|
|
258
|
+
await send_json(send, {"error": "admin_id required"}, 400)
|
|
259
|
+
return
|
|
260
|
+
await send_json(send, route_handlers["admin_admins_remove"](admin_id))
|
|
261
|
+
return
|
|
156
262
|
|
|
157
263
|
await send_json(send, {"error": "not found"}, 404)
|
|
158
264
|
return
|
|
@@ -189,7 +295,7 @@ async def handle_http(
|
|
|
189
295
|
await send_json(send, route_handlers["health"](start_time))
|
|
190
296
|
|
|
191
297
|
elif method == "GET" and path == "/info":
|
|
192
|
-
await send_json(send, route_handlers["info"](trust))
|
|
298
|
+
await send_json(send, route_handlers["info"](trust, trust_config))
|
|
193
299
|
|
|
194
300
|
elif method == "GET" and path == "/docs":
|
|
195
301
|
# Serve static docs page
|