connectonion 0.6.4__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 (33) hide show
  1. connectonion/__init__.py +1 -1
  2. connectonion/cli/co_ai/main.py +2 -2
  3. connectonion/cli/co_ai/prompts/connectonion/concepts/trust.md +166 -208
  4. connectonion/cli/commands/copy_commands.py +21 -0
  5. connectonion/cli/commands/trust_commands.py +152 -0
  6. connectonion/cli/main.py +82 -0
  7. connectonion/core/llm.py +2 -2
  8. connectonion/docs/concepts/fast_rules.md +237 -0
  9. connectonion/docs/concepts/onboarding.md +465 -0
  10. connectonion/docs/concepts/trust.md +933 -192
  11. connectonion/docs/design-decisions/023-trust-policy-system-design.md +323 -0
  12. connectonion/docs/network/README.md +23 -1
  13. connectonion/docs/network/connect.md +135 -0
  14. connectonion/docs/network/host.md +73 -4
  15. connectonion/network/__init__.py +7 -6
  16. connectonion/network/asgi/__init__.py +3 -0
  17. connectonion/network/asgi/http.py +125 -19
  18. connectonion/network/asgi/websocket.py +276 -15
  19. connectonion/network/connect.py +145 -29
  20. connectonion/network/host/auth.py +70 -67
  21. connectonion/network/host/routes.py +88 -3
  22. connectonion/network/host/server.py +100 -17
  23. connectonion/network/trust/__init__.py +27 -19
  24. connectonion/network/trust/factory.py +51 -24
  25. connectonion/network/trust/fast_rules.py +100 -0
  26. connectonion/network/trust/tools.py +316 -32
  27. connectonion/network/trust/trust_agent.py +403 -0
  28. connectonion/transcribe.py +1 -1
  29. {connectonion-0.6.4.dist-info → connectonion-0.6.5.dist-info}/METADATA +1 -1
  30. {connectonion-0.6.4.dist-info → connectonion-0.6.5.dist-info}/RECORD +32 -27
  31. connectonion/network/trust/prompts.py +0 -71
  32. {connectonion-0.6.4.dist-info → connectonion-0.6.5.dist-info}/WHEEL +0 -0
  33. {connectonion-0.6.4.dist-info → connectonion-0.6.5.dist-info}/entry_points.txt +0 -0
@@ -6,14 +6,24 @@ Lifecycle (Client Side):
6
6
  2. agent.input(prompt) opens WebSocket to relay /ws/input endpoint
7
7
  3. Sends INPUT message: {type: "INPUT", input_id, to, prompt, session?}
8
8
  4. Receives streaming events: tool_call, tool_result, thinking, assistant
9
- 5. Receives final OUTPUT: {type: "OUTPUT", result, session} or ask_user
10
- 6. Returns Response(text, done) - done=False means agent asked a question
9
+ 5. If stranger: ONBOARD_REQUIRED callback provides credentials → ONBOARD_SUBMIT → ONBOARD_SUCCESS → retry
10
+ 6. Receives final OUTPUT: {type: "OUTPUT", result, session} or ask_user
11
+ 7. Returns Response(text, done) - done=False means agent asked a question
11
12
 
12
13
  Message Flow:
13
14
  Client → /ws/input → Relay → forwards to Agent's /ws/announce connection
14
15
  Agent processes → sends OUTPUT → Relay resolves pending_outputs future
15
16
  Relay → forwards OUTPUT → Client receives response
16
17
 
18
+ Onboard Flow (for strangers connecting to careful/strict agents):
19
+ Client: INPUT
20
+ Server: ONBOARD_REQUIRED {methods: ["invite_code", "payment"], payment_amount: N}
21
+ Client: calls on_onboard callback to get credentials
22
+ Client: ONBOARD_SUBMIT {invite_code: "...", payment: N}
23
+ Server: ONBOARD_SUCCESS {level: "contact", message: "..."}
24
+ Client: INPUT (retry original prompt)
25
+ Server: OUTPUT
26
+
17
27
  Related Files:
18
28
  - oo-api/relay/routes.py: Relay server WebSocket endpoints
19
29
  - connectonion/network/relay.py: Agent-side relay connection (serve_loop)
@@ -31,7 +41,7 @@ import json
31
41
  import time
32
42
  import uuid
33
43
  from dataclasses import dataclass
34
- from typing import Any, Dict, List, Optional
44
+ from typing import Any, Callable, Dict, List, Optional
35
45
 
36
46
  from .. import address as addr
37
47
 
@@ -96,7 +106,12 @@ class RemoteAgent:
96
106
  """
97
107
  return self._ui_events
98
108
 
99
- def input(self, prompt: str, timeout: float = 60.0) -> Response:
109
+ def input(
110
+ self,
111
+ prompt: str,
112
+ timeout: float = 60.0,
113
+ on_onboard: Optional[Callable[[List[str], Optional[float]], Dict[str, Any]]] = None
114
+ ) -> Response:
100
115
  """
101
116
  Send prompt to remote agent and get response.
102
117
 
@@ -107,6 +122,10 @@ class RemoteAgent:
107
122
  Args:
108
123
  prompt: Task/prompt to send
109
124
  timeout: Seconds to wait for response (default 60)
125
+ on_onboard: Callback when agent requires onboarding (invite code or payment).
126
+ Called with (methods: list[str], payment_amount: float | None).
127
+ Should return {"invite_code": "..."} or {"payment": amount}.
128
+ If None, prompts interactively in terminal.
110
129
 
111
130
  Returns:
112
131
  Response with text and done flag
@@ -115,6 +134,12 @@ class RemoteAgent:
115
134
  >>> response = agent.input("Book a flight to Tokyo")
116
135
  >>> if not response.done:
117
136
  ... response = agent.input("March 15") # Answer the question
137
+
138
+ Onboard example:
139
+ >>> def handle_onboard(methods, payment_amount):
140
+ ... if "invite_code" in methods:
141
+ ... return {"invite_code": input("Enter invite code: ")}
142
+ >>> response = agent.input("Hello", on_onboard=handle_onboard)
118
143
  """
119
144
  try:
120
145
  asyncio.get_running_loop()
@@ -125,11 +150,16 @@ class RemoteAgent:
125
150
  except RuntimeError as e:
126
151
  if "input() cannot be used" in str(e):
127
152
  raise
128
- return asyncio.run(self._stream_input(prompt, timeout))
153
+ return asyncio.run(self._stream_input(prompt, timeout, on_onboard))
129
154
 
130
- async def input_async(self, prompt: str, timeout: float = 60.0) -> Response:
155
+ async def input_async(
156
+ self,
157
+ prompt: str,
158
+ timeout: float = 60.0,
159
+ on_onboard: Optional[Callable[[List[str], Optional[float]], Dict[str, Any]]] = None
160
+ ) -> Response:
131
161
  """Async version of input()."""
132
- return await self._stream_input(prompt, timeout)
162
+ return await self._stream_input(prompt, timeout, on_onboard)
133
163
 
134
164
  def reset(self) -> None:
135
165
  """Clear conversation and start fresh."""
@@ -137,7 +167,12 @@ class RemoteAgent:
137
167
  self._ui_events = []
138
168
  self._status = "idle"
139
169
 
140
- async def _stream_input(self, prompt: str, timeout: float) -> Response:
170
+ async def _stream_input(
171
+ self,
172
+ prompt: str,
173
+ timeout: float,
174
+ on_onboard: Optional[Callable[[List[str], Optional[float]], Dict[str, Any]]] = None
175
+ ) -> Response:
141
176
  """Send prompt via WebSocket and stream events."""
142
177
  import websockets
143
178
 
@@ -153,30 +188,10 @@ class RemoteAgent:
153
188
  ws_url = f"{self._relay_url}/ws/input"
154
189
 
155
190
  # Generate input_id for routing/response matching
156
- import uuid
157
191
  input_id = str(uuid.uuid4())
158
192
 
159
193
  # Build the INPUT message
160
- input_msg = {
161
- "type": "INPUT",
162
- "input_id": input_id,
163
- "prompt": prompt,
164
- "to": self.address,
165
- "timestamp": int(time.time())
166
- }
167
-
168
- # Add session for conversation continuation
169
- if self._current_session:
170
- input_msg["session"] = self._current_session
171
-
172
- # Sign if keys provided
173
- if self._keys:
174
- payload = {"prompt": prompt, "to": self.address, "timestamp": input_msg["timestamp"]}
175
- canonical = json.dumps(payload, sort_keys=True, separators=(',', ':'))
176
- signature = addr.sign(self._keys, canonical.encode())
177
- input_msg["payload"] = payload
178
- input_msg["from"] = self._keys["address"]
179
- input_msg["signature"] = signature.hex()
194
+ input_msg = self._build_input_message(prompt, input_id)
180
195
 
181
196
  try:
182
197
  async with websockets.connect(ws_url) as ws:
@@ -209,6 +224,43 @@ class RemoteAgent:
209
224
  self._status = "idle"
210
225
  raise ConnectionError(f"Agent error: {event.get('message', event.get('error'))}")
211
226
 
227
+ elif event_type == "ONBOARD_REQUIRED":
228
+ # Agent requires onboarding (invite code or payment)
229
+ methods = event.get("methods", [])
230
+ payment_amount = event.get("payment_amount")
231
+
232
+ # Add onboard_required event to UI
233
+ self._add_ui_event({
234
+ "type": "onboard_required",
235
+ "methods": methods,
236
+ "payment_amount": payment_amount
237
+ })
238
+
239
+ # Get credentials from callback or prompt interactively
240
+ if on_onboard:
241
+ credentials = on_onboard(methods, payment_amount)
242
+ else:
243
+ credentials = self._prompt_onboard(methods, payment_amount)
244
+
245
+ # Send ONBOARD_SUBMIT
246
+ submit_msg = self._build_onboard_submit(credentials)
247
+ await ws.send(json.dumps(submit_msg))
248
+ # Continue loop to wait for ONBOARD_SUCCESS
249
+
250
+ elif event_type == "ONBOARD_SUCCESS":
251
+ # Onboard successful - add to UI
252
+ self._add_ui_event({
253
+ "type": "onboard_success",
254
+ "level": event.get("level", "contact"),
255
+ "message": event.get("message", "Onboard successful")
256
+ })
257
+
258
+ # Retry the original prompt
259
+ retry_input_id = str(uuid.uuid4())
260
+ retry_msg = self._build_input_message(prompt, retry_input_id)
261
+ await ws.send(json.dumps(retry_msg))
262
+ # Continue loop to wait for OUTPUT
263
+
212
264
  elif event_type == "ask_user":
213
265
  # Agent is asking a question - return done=False so caller sends another input()
214
266
  self._status = "waiting"
@@ -233,6 +285,70 @@ class RemoteAgent:
233
285
  self._status = "idle"
234
286
  raise TimeoutError(f"Request timed out after {timeout}s")
235
287
 
288
+ def _build_input_message(self, prompt: str, input_id: str) -> Dict[str, Any]:
289
+ """Build INPUT message with optional signing."""
290
+ input_msg = {
291
+ "type": "INPUT",
292
+ "input_id": input_id,
293
+ "prompt": prompt,
294
+ "to": self.address,
295
+ "timestamp": int(time.time())
296
+ }
297
+
298
+ # Add session for conversation continuation
299
+ if self._current_session:
300
+ input_msg["session"] = self._current_session
301
+
302
+ # Sign if keys provided
303
+ if self._keys:
304
+ payload = {"prompt": prompt, "to": self.address, "timestamp": input_msg["timestamp"]}
305
+ canonical = json.dumps(payload, sort_keys=True, separators=(',', ':'))
306
+ signature = addr.sign(self._keys, canonical.encode())
307
+ input_msg["payload"] = payload
308
+ input_msg["from"] = self._keys["address"]
309
+ input_msg["signature"] = signature.hex()
310
+
311
+ return input_msg
312
+
313
+ def _build_onboard_submit(self, credentials: Dict[str, Any]) -> Dict[str, Any]:
314
+ """Build ONBOARD_SUBMIT message with optional signing."""
315
+ payload = {
316
+ "timestamp": int(time.time()),
317
+ **credentials
318
+ }
319
+
320
+ submit_msg: Dict[str, Any] = {
321
+ "type": "ONBOARD_SUBMIT",
322
+ "payload": payload
323
+ }
324
+
325
+ # Sign if keys provided
326
+ if self._keys:
327
+ canonical = json.dumps(payload, sort_keys=True, separators=(',', ':'))
328
+ signature = addr.sign(self._keys, canonical.encode())
329
+ submit_msg["from"] = self._keys["address"]
330
+ submit_msg["signature"] = signature.hex()
331
+
332
+ return submit_msg
333
+
334
+ def _prompt_onboard(self, methods: List[str], payment_amount: Optional[float]) -> Dict[str, Any]:
335
+ """Prompt user interactively for onboard credentials."""
336
+ print(f"\n🔐 Access verification required")
337
+ print(f" Available methods: {', '.join(methods)}")
338
+
339
+ if "invite_code" in methods:
340
+ code = input(" Enter invite code: ").strip()
341
+ if code:
342
+ return {"invite_code": code}
343
+
344
+ if "payment" in methods and payment_amount:
345
+ print(f" Payment required: ${payment_amount}")
346
+ confirm = input(" Pay now? [y/N]: ").strip().lower()
347
+ if confirm == 'y':
348
+ return {"payment": payment_amount}
349
+
350
+ raise ValueError("No valid onboard credentials provided")
351
+
236
352
  def _handle_stream_event(self, event: Dict[str, Any]) -> None:
237
353
  """Handle streaming event and update UI."""
238
354
  event_type = event.get("type")
@@ -1,20 +1,25 @@
1
1
  """
2
2
  Purpose: Ed25519 signature verification and trust-based authentication for hosted agents
3
3
  LLM-Note:
4
- Dependencies: imports from [network/trust/, llm_do.py, nacl.signing] | imported by [network/host/routes.py, network/host/server.py] | tested by [tests/network/test_host_auth.py]
5
- Data flow: receives request dict with {payload, from, signature} → extract_and_authenticate() verifies Ed25519 signature via verify_signature() using nacl checks timestamp expiry (5 min window) applies trust policy (open/careful/strict/custom) optionally evaluates via trust agent → returns (prompt, identity, sig_valid, error)
6
- State/Effects: reads trust settings from agent | calls trust agent for evaluation if custom policy | no persistent state
7
- Integration: exposes verify_signature(payload, signature, public_key) → bool, extract_and_authenticate(data, trust, blacklist, whitelist, agent_address) → (prompt, identity, sig_valid, error), get_agent_address(agent) → str, is_custom_trust(trust) → bool | used by host() to enforce authentication on all requests
8
- Performance: signature verification uses nacl (fast Ed25519) | 5 minute timestamp window prevents replay | whitelist bypass for trusted callers
9
- Errors: returns error strings: "unauthorized: ...", "forbidden: ...", "rejected: ..." | does NOT raise exceptions (caller checks error)
4
+ Dependencies: imports from [network/trust/TrustAgent, nacl.signing] | imported by [network/host/routes.py, network/host/server.py] | tested by [tests/unit/test_host_auth.py]
5
+ Data flow: receives request dict with {payload, from, signature} → extract_and_authenticate() verifies Ed25519 signature → uses TrustAgent.should_allow() for trust decisions (fast rules + LLM fallback) → returns (prompt, identity, sig_valid, error)
6
+ State/Effects: TrustAgent handles all trust state (whitelist, contacts, blocklist in ~/.co/)
7
+ Integration: exposes verify_signature(), extract_and_authenticate(), get_agent_address(), is_custom_trust() | used by host() to enforce authentication
8
+ Performance: TrustAgent.should_allow() runs fast rules first (zero tokens), only uses LLM for 'ask' cases
9
+ Errors: returns error strings: "unauthorized: ...", "forbidden: ..." | does NOT raise exceptions
10
10
  Authentication and signature verification for hosted agents.
11
+
12
+ Trust evaluation (via TrustAgent.should_allow()):
13
+ 1. Parameter whitelist (highest priority, instant allow)
14
+ 2. Signature verification (protocol level)
15
+ 3. TrustAgent handles fast rules + LLM fallback
11
16
  """
12
17
 
13
18
  import hashlib
14
19
  import json
15
20
  import time
16
21
 
17
- from ..trust import create_trust_agent, TRUST_LEVELS
22
+ from ..trust import TrustAgent, TRUST_LEVELS
18
23
 
19
24
 
20
25
  # Signature expiry window (5 minutes)
@@ -64,10 +69,29 @@ def extract_and_authenticate(data: dict, trust, *, blacklist=None, whitelist=Non
64
69
  "signature": "0xEd25519Signature..."
65
70
  }
66
71
 
67
- Trust levels control additional policies AFTER signature verification:
68
- - "open": Any valid signer allowed
69
- - "careful": Warnings for unknown signers (default)
70
- - "strict": Whitelist only
72
+ Onboarding (in payload):
73
+ {
74
+ "payload": {"prompt": "...", "invite_code": "BETA2024", ...}
75
+ }
76
+ or:
77
+ {
78
+ "payload": {"prompt": "...", "payment": 10, ...}
79
+ }
80
+
81
+ Authentication flow:
82
+ 1. Signature verification (protocol level, always required)
83
+ 2. Parameter whitelist check (instant allow if match)
84
+ 3. Fast rules from YAML config:
85
+ - allow: [whitelisted, contact] → instant allow
86
+ - deny: [blocked] → instant deny
87
+ - onboard: {invite_code, payment} → promote stranger to contact
88
+ - default: allow | deny | ask → final decision
89
+ 4. Trust agent (only if fast rules return None for 'ask' cases)
90
+
91
+ Trust levels (predefined YAML configs):
92
+ - "open": default=allow (development)
93
+ - "careful": default=ask (staging)
94
+ - "strict": allow=[whitelisted], default=deny (production)
71
95
  - Custom policy/Agent: LLM evaluation
72
96
 
73
97
  Returns: (prompt, identity, sig_valid, error)
@@ -76,31 +100,48 @@ def extract_and_authenticate(data: dict, trust, *, blacklist=None, whitelist=Non
76
100
  if "payload" not in data or "signature" not in data:
77
101
  return None, None, False, "unauthorized: signed request required"
78
102
 
79
- # Verify signature (protocol level - always required)
103
+ # Verify signature (protocol level - always required, even for whitelisted)
80
104
  prompt, identity, error = _authenticate_signed(
81
- data, blacklist=blacklist, whitelist=whitelist, agent_address=agent_address
105
+ data, blacklist=blacklist, agent_address=agent_address
82
106
  )
83
107
  if error:
84
108
  return prompt, identity, False, error
85
109
 
86
- # Trust level: additional policies AFTER signature verification
87
- if trust == "strict" and whitelist and identity not in whitelist:
88
- return None, identity, True, "forbidden: not in whitelist"
89
-
90
- # Custom trust policy/agent evaluation
91
- if is_custom_trust(trust):
92
- trust_agent = create_trust_agent(trust)
93
- accepted, reason = evaluate_with_trust_agent(trust_agent, prompt, identity, True)
94
- if not accepted:
95
- return None, identity, True, f"rejected: {reason}"
96
-
97
- return prompt, identity, True, None
98
-
110
+ # Parameter whitelist bypasses trust POLICY (not signature verification)
111
+ if whitelist and identity in whitelist:
112
+ return prompt, identity, True, None
99
113
 
100
- def _authenticate_signed(data: dict, *, blacklist=None, whitelist=None, agent_address=None):
114
+ # Use TrustAgent for all trust decisions (fast rules + LLM fallback)
115
+ payload = data.get("payload", {})
116
+ request_data = {
117
+ "prompt": prompt,
118
+ "invite_code": payload.get("invite_code"),
119
+ "payment": payload.get("payment", 0),
120
+ }
121
+
122
+ # Trust should be TrustAgent (resolved by host/create_app)
123
+ # But handle string for backwards compatibility with direct calls
124
+ if isinstance(trust, TrustAgent):
125
+ trust_agent = trust
126
+ elif isinstance(trust, str):
127
+ trust_agent = TrustAgent(trust)
128
+ else:
129
+ # Unknown type (e.g., Agent) - use default "careful"
130
+ trust_agent = TrustAgent("careful")
131
+
132
+ decision = trust_agent.should_allow(identity, request_data)
133
+
134
+ if decision.allow:
135
+ return prompt, identity, True, None
136
+ else:
137
+ return None, identity, True, f"forbidden: {decision.reason}"
138
+
139
+
140
+ def _authenticate_signed(data: dict, *, blacklist=None, agent_address=None):
101
141
  """Authenticate signed request with Ed25519 - ALWAYS REQUIRED.
102
142
 
103
143
  Protocol-level signature verification. All requests must be signed.
144
+ Whitelist is NOT checked here - it bypasses trust policy, not signature.
104
145
 
105
146
  Returns: (prompt, identity, error) - error is None on success
106
147
  """
@@ -112,14 +153,10 @@ def _authenticate_signed(data: dict, *, blacklist=None, whitelist=None, agent_ad
112
153
  timestamp = payload.get("timestamp")
113
154
  to_address = payload.get("to")
114
155
 
115
- # Check blacklist first
156
+ # Check blacklist first (security: even before signature check)
116
157
  if blacklist and identity in blacklist:
117
158
  return None, identity, "forbidden: blacklisted"
118
159
 
119
- # Check whitelist (bypass signature check - trusted caller)
120
- if whitelist and identity in whitelist:
121
- return prompt, identity, None
122
-
123
160
  # Validate required fields
124
161
  if not identity:
125
162
  return None, None, "unauthorized: 'from' field required"
@@ -137,7 +174,7 @@ def _authenticate_signed(data: dict, *, blacklist=None, whitelist=None, agent_ad
137
174
  if agent_address and to_address and to_address != agent_address:
138
175
  return None, identity, "unauthorized: wrong recipient"
139
176
 
140
- # Verify signature
177
+ # Verify signature ALWAYS (no whitelist bypass - that's at policy level)
141
178
  if not verify_signature(payload, signature, identity):
142
179
  return None, identity, "unauthorized: invalid signature"
143
180
 
@@ -150,40 +187,6 @@ def get_agent_address(agent) -> str:
150
187
  return f"0x{h[:40]}"
151
188
 
152
189
 
153
- def evaluate_with_trust_agent(trust_agent, prompt: str, identity: str, sig_valid: bool) -> tuple[bool, str]:
154
- """Evaluate request using a custom trust agent (policy or Agent).
155
-
156
- Only called when trust is a policy string or custom Agent - NOT for simple levels.
157
-
158
- Args:
159
- trust_agent: The trust agent created from policy or custom Agent
160
- prompt: The request prompt
161
- identity: The requester's identity/address
162
- sig_valid: Whether the signature is valid
163
-
164
- Returns:
165
- (accepted, reason) tuple
166
- """
167
- from pydantic import BaseModel
168
- from ...llm_do import llm_do
169
-
170
- class TrustDecision(BaseModel):
171
- accept: bool
172
- reason: str
173
-
174
- request_info = f"""Evaluate this request:
175
- - prompt: {prompt[:200]}{'...' if len(prompt) > 200 else ''}
176
- - identity: {identity or 'anonymous'}
177
- - signature_valid: {sig_valid}"""
178
-
179
- decision = llm_do(
180
- request_info,
181
- output=TrustDecision,
182
- system_prompt=trust_agent.system_prompt,
183
- )
184
- return decision.accept, decision.reason
185
-
186
-
187
190
  def is_custom_trust(trust) -> bool:
188
191
  """Check if trust needs a custom agent (policy or Agent, not a level)."""
189
192
  if not isinstance(trust, str):
@@ -88,13 +88,19 @@ def health_handler(agent_name: str, start_time: float) -> dict:
88
88
  return {"status": "healthy", "agent": agent_name, "uptime": int(time.time() - start_time)}
89
89
 
90
90
 
91
- def info_handler(agent_metadata: dict, trust: str) -> dict:
91
+ def info_handler(agent_metadata: dict, trust: str, trust_config: dict | None = None) -> dict:
92
92
  """GET /info
93
93
 
94
- Returns pre-extracted metadata - no agent creation needed.
94
+ Returns pre-extracted metadata including onboard requirements.
95
+
96
+ Args:
97
+ agent_metadata: Agent name, address, tools
98
+ trust: Trust level string ("open", "careful", "strict", or custom)
99
+ trust_config: Parsed YAML config from trust policy (optional)
95
100
  """
96
101
  from ... import __version__
97
- return {
102
+
103
+ result = {
98
104
  "name": agent_metadata["name"],
99
105
  "address": agent_metadata["address"],
100
106
  "tools": agent_metadata["tools"],
@@ -102,6 +108,17 @@ def info_handler(agent_metadata: dict, trust: str) -> dict:
102
108
  "version": __version__,
103
109
  }
104
110
 
111
+ # Add onboard info if available
112
+ if trust_config:
113
+ onboard = trust_config.get("onboard", {})
114
+ if onboard:
115
+ result["onboard"] = {
116
+ "invite_code": "invite_code" in onboard,
117
+ "payment": onboard.get("payment"),
118
+ }
119
+
120
+ return result
121
+
105
122
 
106
123
  def admin_logs_handler(agent_name: str) -> dict:
107
124
  """GET /admin/logs - return plain text activity log file."""
@@ -133,3 +150,71 @@ def admin_sessions_handler() -> dict:
133
150
  # Sort by updated date descending (newest first)
134
151
  sessions.sort(key=lambda s: s.get("updated", s.get("created", "")), reverse=True)
135
152
  return {"sessions": sessions}
153
+
154
+
155
+ # === Admin Trust Routes ===
156
+ # These receive TrustAgent instance from route_handlers
157
+
158
+ def admin_trust_promote_handler(trust_agent, client_id: str) -> dict:
159
+ """POST /admin/trust/promote - Promote client to next level."""
160
+ level = trust_agent.get_level(client_id)
161
+ if level == "stranger":
162
+ result = trust_agent.promote_to_contact(client_id)
163
+ elif level == "contact":
164
+ result = trust_agent.promote_to_whitelist(client_id)
165
+ elif level == "whitelist":
166
+ return {"error": "Already at highest level", "level": level}
167
+ elif level == "blocked":
168
+ return {"error": "Client is blocked. Unblock first.", "level": level}
169
+ else:
170
+ return {"error": f"Unknown level: {level}"}
171
+
172
+ return {"success": True, "message": result, "level": trust_agent.get_level(client_id)}
173
+
174
+
175
+ def admin_trust_demote_handler(trust_agent, client_id: str) -> dict:
176
+ """POST /admin/trust/demote - Demote client to previous level."""
177
+ level = trust_agent.get_level(client_id)
178
+ if level == "whitelist":
179
+ result = trust_agent.demote_to_contact(client_id)
180
+ elif level == "contact":
181
+ result = trust_agent.demote_to_stranger(client_id)
182
+ elif level == "stranger":
183
+ return {"error": "Already at lowest level", "level": level}
184
+ elif level == "blocked":
185
+ return {"error": "Client is blocked. Unblock first.", "level": level}
186
+ else:
187
+ return {"error": f"Unknown level: {level}"}
188
+
189
+ return {"success": True, "message": result, "level": trust_agent.get_level(client_id)}
190
+
191
+
192
+ def admin_trust_block_handler(trust_agent, client_id: str, reason: str = "") -> dict:
193
+ """POST /admin/trust/block - Block a client."""
194
+ result = trust_agent.block(client_id, reason)
195
+ return {"success": True, "message": result, "level": trust_agent.get_level(client_id)}
196
+
197
+
198
+ def admin_trust_unblock_handler(trust_agent, client_id: str) -> dict:
199
+ """POST /admin/trust/unblock - Unblock a client."""
200
+ result = trust_agent.unblock(client_id)
201
+ return {"success": True, "message": result, "level": trust_agent.get_level(client_id)}
202
+
203
+
204
+ def admin_trust_level_handler(trust_agent, client_id: str) -> dict:
205
+ """GET /admin/trust/level/{client_id} - Get client's trust level."""
206
+ return {"client_id": client_id, "level": trust_agent.get_level(client_id)}
207
+
208
+
209
+ # === Super Admin Routes (admin management) ===
210
+
211
+ def admin_admins_add_handler(trust_agent, admin_id: str) -> dict:
212
+ """POST /superadmin/add - Add an admin. Super admin only."""
213
+ result = trust_agent.add_admin(admin_id)
214
+ return {"success": True, "message": result}
215
+
216
+
217
+ def admin_admins_remove_handler(trust_agent, admin_id: str) -> dict:
218
+ """POST /superadmin/remove - Remove an admin. Super admin only."""
219
+ result = trust_agent.remove_admin(admin_id)
220
+ return {"success": True, "message": result}