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.
- connectonion/__init__.py +1 -1
- connectonion/cli/co_ai/main.py +2 -2
- connectonion/cli/co_ai/prompts/connectonion/concepts/trust.md +166 -208
- 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/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/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-0.6.4.dist-info → connectonion-0.6.5.dist-info}/METADATA +1 -1
- {connectonion-0.6.4.dist-info → connectonion-0.6.5.dist-info}/RECORD +32 -27
- connectonion/network/trust/prompts.py +0 -71
- {connectonion-0.6.4.dist-info → connectonion-0.6.5.dist-info}/WHEEL +0 -0
- {connectonion-0.6.4.dist-info → connectonion-0.6.5.dist-info}/entry_points.txt +0 -0
connectonion/network/connect.py
CHANGED
|
@@ -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.
|
|
10
|
-
6.
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
5
|
-
Data flow: receives request dict with {payload, from, signature} → extract_and_authenticate() verifies Ed25519 signature
|
|
6
|
-
State/Effects:
|
|
7
|
-
Integration: exposes verify_signature(
|
|
8
|
-
Performance:
|
|
9
|
-
Errors: returns error strings: "unauthorized: ...", "forbidden: ..."
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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,
|
|
105
|
+
data, blacklist=blacklist, agent_address=agent_address
|
|
82
106
|
)
|
|
83
107
|
if error:
|
|
84
108
|
return prompt, identity, False, error
|
|
85
109
|
|
|
86
|
-
#
|
|
87
|
-
if
|
|
88
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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}
|