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.
Files changed (50) hide show
  1. connectonion/__init__.py +1 -1
  2. connectonion/cli/co_ai/agent.py +3 -3
  3. connectonion/cli/co_ai/main.py +2 -2
  4. connectonion/cli/co_ai/plugins/__init__.py +2 -3
  5. connectonion/cli/co_ai/plugins/system_reminder.py +154 -0
  6. connectonion/cli/co_ai/prompts/connectonion/concepts/trust.md +166 -208
  7. connectonion/cli/co_ai/prompts/system-reminders/agent.md +23 -0
  8. connectonion/cli/co_ai/prompts/system-reminders/plan_mode.md +13 -0
  9. connectonion/cli/co_ai/prompts/system-reminders/security.md +14 -0
  10. connectonion/cli/co_ai/prompts/system-reminders/simplicity.md +14 -0
  11. connectonion/cli/co_ai/tools/plan_mode.py +1 -4
  12. connectonion/cli/co_ai/tools/read.py +0 -6
  13. connectonion/cli/commands/copy_commands.py +21 -0
  14. connectonion/cli/commands/trust_commands.py +152 -0
  15. connectonion/cli/main.py +82 -0
  16. connectonion/core/llm.py +2 -2
  17. connectonion/docs/concepts/fast_rules.md +237 -0
  18. connectonion/docs/concepts/onboarding.md +465 -0
  19. connectonion/docs/concepts/plugins.md +2 -1
  20. connectonion/docs/concepts/trust.md +933 -192
  21. connectonion/docs/design-decisions/023-trust-policy-system-design.md +323 -0
  22. connectonion/docs/network/README.md +23 -1
  23. connectonion/docs/network/connect.md +135 -0
  24. connectonion/docs/network/host.md +73 -4
  25. connectonion/docs/useful_plugins/tool_approval.md +139 -0
  26. connectonion/network/__init__.py +7 -6
  27. connectonion/network/asgi/__init__.py +3 -0
  28. connectonion/network/asgi/http.py +125 -19
  29. connectonion/network/asgi/websocket.py +276 -15
  30. connectonion/network/connect.py +145 -29
  31. connectonion/network/host/auth.py +70 -67
  32. connectonion/network/host/routes.py +88 -3
  33. connectonion/network/host/server.py +100 -17
  34. connectonion/network/trust/__init__.py +27 -19
  35. connectonion/network/trust/factory.py +51 -24
  36. connectonion/network/trust/fast_rules.py +100 -0
  37. connectonion/network/trust/tools.py +316 -32
  38. connectonion/network/trust/trust_agent.py +403 -0
  39. connectonion/transcribe.py +1 -1
  40. connectonion/useful_plugins/__init__.py +2 -1
  41. connectonion/useful_plugins/tool_approval.py +233 -0
  42. {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/METADATA +1 -1
  43. {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/RECORD +45 -37
  44. connectonion/cli/co_ai/plugins/reminder.py +0 -76
  45. connectonion/cli/co_ai/plugins/shell_approval.py +0 -105
  46. connectonion/cli/co_ai/prompts/reminders/plan_mode.md +0 -34
  47. connectonion/cli/co_ai/reminders.py +0 -159
  48. connectonion/network/trust/prompts.py +0 -71
  49. {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/WHEEL +0 -0
  50. {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
@@ -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 (create_trust_agent, TRUST_LEVELS, prompts) | unified networking API surface
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 create_trust_agent, get_default_trust_level, TRUST_LEVELS, get_trust_prompt, TRUST_PROMPTS
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
- "create_trust_agent",
43
+ # Trust (TrustAgent is the single interface)
44
+ "TrustAgent",
45
+ "Decision",
44
46
  "get_default_trust_level",
45
47
  "TRUST_LEVELS",
46
- "get_trust_prompt",
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 require API key auth
138
+ # Admin endpoints
137
139
  if path.startswith("/admin"):
138
- headers = dict(scope.get("headers", []))
139
- auth = headers.get(b"authorization", b"").decode()
140
- expected = os.environ.get("OPENONION_API_KEY", "")
141
- if not expected or not auth.startswith("Bearer ") or not hmac.compare_digest(auth[7:], expected):
142
- await send_json(send, {"error": "unauthorized"}, 401)
143
- return
144
-
145
- if method == "GET" and path == "/admin/logs":
146
- result = route_handlers["admin_logs"]()
147
- if "error" in result:
148
- await send_json(send, result, 404)
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
- await send_text(send, result["content"])
151
- return
152
-
153
- if method == "GET" and path == "/admin/sessions":
154
- await send_json(send, route_handlers["admin_sessions"]())
155
- return
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