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
|
@@ -26,6 +26,7 @@ don't interfere between concurrent requests.
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
import os
|
|
29
|
+
from functools import partial
|
|
29
30
|
from pathlib import Path
|
|
30
31
|
from typing import Callable, Union
|
|
31
32
|
|
|
@@ -33,7 +34,8 @@ from rich.console import Console
|
|
|
33
34
|
from rich.panel import Panel
|
|
34
35
|
|
|
35
36
|
from ..asgi import create_app as asgi_create_app
|
|
36
|
-
from ..trust import get_default_trust_level
|
|
37
|
+
from ..trust import TrustAgent, get_default_trust_level, parse_policy, TRUST_LEVELS
|
|
38
|
+
from ..trust.factory import PROMPTS_DIR
|
|
37
39
|
from .session import SessionStorage
|
|
38
40
|
from .auth import extract_and_authenticate
|
|
39
41
|
from .routes import (
|
|
@@ -44,9 +46,49 @@ from .routes import (
|
|
|
44
46
|
info_handler,
|
|
45
47
|
admin_logs_handler,
|
|
46
48
|
admin_sessions_handler,
|
|
49
|
+
# Admin trust routes
|
|
50
|
+
admin_trust_promote_handler,
|
|
51
|
+
admin_trust_demote_handler,
|
|
52
|
+
admin_trust_block_handler,
|
|
53
|
+
admin_trust_unblock_handler,
|
|
54
|
+
admin_trust_level_handler,
|
|
55
|
+
# Super admin routes
|
|
56
|
+
admin_admins_add_handler,
|
|
57
|
+
admin_admins_remove_handler,
|
|
47
58
|
)
|
|
48
59
|
|
|
49
60
|
|
|
61
|
+
def _parse_trust_config(trust: Union[str, "Agent"]) -> dict | None:
|
|
62
|
+
"""Parse trust config from trust parameter.
|
|
63
|
+
|
|
64
|
+
Returns YAML config dict if trust is a level or file path, None otherwise.
|
|
65
|
+
Used to extract onboard info for /info endpoint.
|
|
66
|
+
"""
|
|
67
|
+
if not isinstance(trust, str):
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# Check if it's a trust level
|
|
71
|
+
if trust.lower() in TRUST_LEVELS:
|
|
72
|
+
policy_path = PROMPTS_DIR / f"{trust.lower()}.md"
|
|
73
|
+
if policy_path.exists():
|
|
74
|
+
config, _ = parse_policy(policy_path.read_text(encoding='utf-8'))
|
|
75
|
+
return config
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Check if it's a file path
|
|
79
|
+
path = Path(trust)
|
|
80
|
+
if path.exists() and path.is_file():
|
|
81
|
+
config, _ = parse_policy(path.read_text(encoding='utf-8'))
|
|
82
|
+
return config
|
|
83
|
+
|
|
84
|
+
# Inline policy text
|
|
85
|
+
if trust.startswith('---'):
|
|
86
|
+
config, _ = parse_policy(trust)
|
|
87
|
+
return config
|
|
88
|
+
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
50
92
|
def get_default_trust() -> str:
|
|
51
93
|
"""Get default trust based on environment.
|
|
52
94
|
|
|
@@ -70,7 +112,7 @@ def _extract_agent_metadata(create_agent: Callable) -> tuple[dict, object]:
|
|
|
70
112
|
return metadata, sample
|
|
71
113
|
|
|
72
114
|
|
|
73
|
-
def _create_route_handlers(create_agent: Callable, agent_metadata: dict, result_ttl: int):
|
|
115
|
+
def _create_route_handlers(create_agent: Callable, agent_metadata: dict, result_ttl: int, trust_agent):
|
|
74
116
|
"""Create route handler dict for ASGI app.
|
|
75
117
|
|
|
76
118
|
Args:
|
|
@@ -79,6 +121,7 @@ def _create_route_handlers(create_agent: Callable, agent_metadata: dict, result_
|
|
|
79
121
|
agent_metadata: Pre-extracted metadata (name, tools, address) - avoids
|
|
80
122
|
creating agents for health/info endpoints.
|
|
81
123
|
result_ttl: How long to keep results on server in seconds
|
|
124
|
+
trust_agent: TrustAgent instance for trust operations
|
|
82
125
|
"""
|
|
83
126
|
agent_name = agent_metadata["name"]
|
|
84
127
|
|
|
@@ -91,8 +134,8 @@ def _create_route_handlers(create_agent: Callable, agent_metadata: dict, result_
|
|
|
91
134
|
def handle_health(start_time):
|
|
92
135
|
return health_handler(agent_name, start_time)
|
|
93
136
|
|
|
94
|
-
def handle_info(trust):
|
|
95
|
-
return info_handler(agent_metadata, trust)
|
|
137
|
+
def handle_info(trust, trust_config=None):
|
|
138
|
+
return info_handler(agent_metadata, trust, trust_config)
|
|
96
139
|
|
|
97
140
|
def handle_admin_logs():
|
|
98
141
|
return admin_logs_handler(agent_name)
|
|
@@ -107,6 +150,17 @@ def _create_route_handlers(create_agent: Callable, agent_metadata: dict, result_
|
|
|
107
150
|
"ws_input": handle_ws_input,
|
|
108
151
|
"admin_logs": handle_admin_logs,
|
|
109
152
|
"admin_sessions": admin_sessions_handler,
|
|
153
|
+
# TrustAgent instance for direct access in http.py/websocket.py
|
|
154
|
+
"trust_agent": trust_agent,
|
|
155
|
+
# Admin trust routes (partial injects trust_agent as first arg)
|
|
156
|
+
"admin_trust_promote": partial(admin_trust_promote_handler, trust_agent),
|
|
157
|
+
"admin_trust_demote": partial(admin_trust_demote_handler, trust_agent),
|
|
158
|
+
"admin_trust_block": partial(admin_trust_block_handler, trust_agent),
|
|
159
|
+
"admin_trust_unblock": partial(admin_trust_unblock_handler, trust_agent),
|
|
160
|
+
"admin_trust_level": partial(admin_trust_level_handler, trust_agent),
|
|
161
|
+
# Super admin routes
|
|
162
|
+
"admin_admins_add": partial(admin_admins_add_handler, trust_agent),
|
|
163
|
+
"admin_admins_remove": partial(admin_admins_remove_handler, trust_agent),
|
|
110
164
|
}
|
|
111
165
|
|
|
112
166
|
|
|
@@ -197,14 +251,14 @@ def host(
|
|
|
197
251
|
whitelist: Allowed identities
|
|
198
252
|
|
|
199
253
|
Endpoints:
|
|
200
|
-
POST /input
|
|
201
|
-
GET /sessions/{id}
|
|
202
|
-
GET /sessions
|
|
203
|
-
GET /health
|
|
204
|
-
GET /info
|
|
205
|
-
WS /ws
|
|
206
|
-
GET /logs
|
|
207
|
-
GET /
|
|
254
|
+
POST /input - Submit prompt, get result
|
|
255
|
+
GET /sessions/{id} - Get session by ID
|
|
256
|
+
GET /sessions - List all sessions
|
|
257
|
+
GET /health - Health check
|
|
258
|
+
GET /info - Agent info
|
|
259
|
+
WS /ws - WebSocket
|
|
260
|
+
GET /admin/logs - Activity log (requires OPENONION_API_KEY)
|
|
261
|
+
GET /admin/sessions - Activity sessions (requires OPENONION_API_KEY)
|
|
208
262
|
"""
|
|
209
263
|
import uvicorn
|
|
210
264
|
from ... import address
|
|
@@ -228,11 +282,24 @@ def host(
|
|
|
228
282
|
agent_metadata["address"] = addr_data['address']
|
|
229
283
|
|
|
230
284
|
storage = SessionStorage()
|
|
231
|
-
|
|
285
|
+
|
|
286
|
+
# Create TrustAgent instance - the single interface for all trust operations
|
|
287
|
+
# Users can subclass TrustAgent to customize (e.g., database-backed admin storage)
|
|
288
|
+
if isinstance(trust, TrustAgent):
|
|
289
|
+
trust_agent = trust
|
|
290
|
+
else:
|
|
291
|
+
trust_agent = TrustAgent(trust if isinstance(trust, str) else "careful")
|
|
292
|
+
|
|
293
|
+
route_handlers = _create_route_handlers(create_agent, agent_metadata, result_ttl, trust_agent)
|
|
294
|
+
|
|
295
|
+
# Parse trust config for /info onboard info
|
|
296
|
+
trust_config = _parse_trust_config(trust)
|
|
297
|
+
|
|
232
298
|
app = asgi_create_app(
|
|
233
299
|
route_handlers=route_handlers,
|
|
234
300
|
storage=storage,
|
|
235
|
-
trust=trust
|
|
301
|
+
trust=trust_agent, # Pass resolved TrustAgent, not raw trust
|
|
302
|
+
trust_config=trust_config,
|
|
236
303
|
blacklist=blacklist,
|
|
237
304
|
whitelist=whitelist,
|
|
238
305
|
)
|
|
@@ -243,13 +310,23 @@ def host(
|
|
|
243
310
|
|
|
244
311
|
# Display startup info
|
|
245
312
|
relay_status = f"[green]✓[/] {relay_url}" if relay_url else "[dim]disabled[/]"
|
|
313
|
+
|
|
314
|
+
# Build trust info for display
|
|
315
|
+
trust_info = ""
|
|
316
|
+
if trust_config and isinstance(trust, str) and trust.lower() in TRUST_LEVELS:
|
|
317
|
+
onboard = trust_config.get("onboard", {})
|
|
318
|
+
invite_codes = onboard.get("invite_code", [])
|
|
319
|
+
if invite_codes:
|
|
320
|
+
codes_str = ", ".join(invite_codes) if isinstance(invite_codes, list) else invite_codes
|
|
321
|
+
trust_info = f"\n[bold]Invite:[/] {codes_str}\n[dim] Run 'co copy trust/{trust}' to customize[/]"
|
|
322
|
+
|
|
246
323
|
Console().print(Panel(
|
|
247
324
|
f"[bold]POST[/] http://localhost:{port}/input\n"
|
|
248
325
|
f"[dim]GET /sessions/{{id}} · /sessions · /health · /info[/]\n"
|
|
249
326
|
f"[dim]WS ws://localhost:{port}/ws\n"
|
|
250
327
|
f"[dim]UI http://localhost:{port}/docs[/]\n\n"
|
|
251
328
|
f"[bold]Address:[/] {agent_metadata['address']}\n"
|
|
252
|
-
f"[bold]Relay:[/] {relay_status}",
|
|
329
|
+
f"[bold]Relay:[/] {relay_status}{trust_info}",
|
|
253
330
|
title=f"[green]Agent '{agent_metadata['name']}'[/]"
|
|
254
331
|
))
|
|
255
332
|
|
|
@@ -279,11 +356,17 @@ def create_app(create_agent: Callable, storage=None, trust="careful", result_ttl
|
|
|
279
356
|
agent_metadata, sample = _extract_agent_metadata(create_agent)
|
|
280
357
|
agent_metadata["address"] = get_agent_address(sample)
|
|
281
358
|
|
|
282
|
-
|
|
359
|
+
# Create TrustAgent instance
|
|
360
|
+
if isinstance(trust, TrustAgent):
|
|
361
|
+
trust_agent = trust
|
|
362
|
+
else:
|
|
363
|
+
trust_agent = TrustAgent(trust if isinstance(trust, str) else "careful")
|
|
364
|
+
|
|
365
|
+
route_handlers = _create_route_handlers(create_agent, agent_metadata, result_ttl, trust_agent)
|
|
283
366
|
return asgi_create_app(
|
|
284
367
|
route_handlers=route_handlers,
|
|
285
368
|
storage=storage,
|
|
286
|
-
trust=trust
|
|
369
|
+
trust=trust_agent, # Pass resolved TrustAgent, not raw trust
|
|
287
370
|
blacklist=blacklist,
|
|
288
371
|
whitelist=whitelist,
|
|
289
372
|
)
|
|
@@ -1,30 +1,38 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Purpose: Trust verification system module re-exporting factory, prompts, and verification tools
|
|
3
|
-
LLM-Note:
|
|
4
|
-
Dependencies: imports from [factory.py, prompts.py, tools.py] | imported by [network/__init__.py, network/host/auth.py, network/host/server.py] | tested by [tests/network/test_trust.py]
|
|
5
|
-
Data flow: re-exports trust agent creation utilities and trust level prompts
|
|
6
|
-
State/Effects: no state
|
|
7
|
-
Integration: exposes create_trust_agent(trust_spec) → Agent, get_default_trust_level() → str, validate_trust_level(level), TRUST_LEVELS dict, TRUST_PROMPTS dict, get_trust_prompt(level) → str, get_trust_verification_tools() → list | used by host() for access control policies
|
|
8
|
-
Performance: trivial
|
|
9
|
-
Errors: none
|
|
10
2
|
Trust verification system for agent networking.
|
|
11
3
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
Usage:
|
|
5
|
+
from connectonion.network.trust import TrustAgent
|
|
6
|
+
|
|
7
|
+
trust = TrustAgent("careful") # or "open", "strict", or path to policy
|
|
8
|
+
|
|
9
|
+
# Access control
|
|
10
|
+
decision = trust.should_allow("client-123", {"prompt": "hello"})
|
|
11
|
+
|
|
12
|
+
# Trust level operations
|
|
13
|
+
trust.promote_to_contact("client-123")
|
|
14
|
+
trust.block("bad-actor", reason="spam")
|
|
15
|
+
level = trust.get_level("client-123") # "stranger", "contact", "whitelist", "blocked"
|
|
16
|
+
|
|
17
|
+
# Admin operations
|
|
18
|
+
trust.is_admin("client-123")
|
|
19
|
+
trust.add_admin("new-admin")
|
|
20
|
+
|
|
21
|
+
TrustAgent is the single interface for all trust operations.
|
|
22
|
+
Subclass to customize behavior (e.g., database-backed admin storage).
|
|
16
23
|
"""
|
|
17
24
|
|
|
18
|
-
from .
|
|
19
|
-
from .
|
|
20
|
-
from .
|
|
25
|
+
from .trust_agent import TrustAgent, Decision
|
|
26
|
+
from .factory import get_default_trust_level, validate_trust_level, TRUST_LEVELS
|
|
27
|
+
from .fast_rules import parse_policy
|
|
21
28
|
|
|
22
29
|
__all__ = [
|
|
23
|
-
|
|
30
|
+
# TrustAgent - the single interface
|
|
31
|
+
"TrustAgent",
|
|
32
|
+
"Decision",
|
|
33
|
+
# Helpers
|
|
24
34
|
"get_default_trust_level",
|
|
25
35
|
"validate_trust_level",
|
|
26
36
|
"TRUST_LEVELS",
|
|
27
|
-
"
|
|
28
|
-
"get_trust_prompt",
|
|
29
|
-
"get_trust_verification_tools",
|
|
37
|
+
"parse_policy",
|
|
30
38
|
]
|
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Factory for creating trust verification agents with policies.
|
|
3
|
+
|
|
4
|
+
Policy files use YAML frontmatter for fast rules + markdown body for LLM prompts:
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
allow: [whitelisted, contact]
|
|
8
|
+
deny: [blocked]
|
|
9
|
+
onboard:
|
|
10
|
+
invite_code: [BETA2024]
|
|
11
|
+
payment: 10
|
|
12
|
+
default: ask
|
|
13
|
+
---
|
|
14
|
+
# LLM Prompt for trust evaluation...
|
|
15
|
+
|
|
16
|
+
Fast rules execute without LLM (zero tokens, instant). Only 'default: ask' triggers LLM.
|
|
3
17
|
|
|
4
18
|
String Resolution Priority:
|
|
5
|
-
1. Trust level ("open", "careful", "strict")
|
|
19
|
+
1. Trust level ("open", "careful", "strict") → loads from prompts/trust/{level}.md
|
|
6
20
|
2. File path (if exists)
|
|
7
21
|
3. Inline policy text
|
|
8
|
-
|
|
9
|
-
LLM-Note:
|
|
10
|
-
Dependencies: [os, pathlib, typing, .prompts, .tools] | imported by [host/server.py] | tested by [tests/unit/test_trust.py]
|
|
11
|
-
Data flow: trust param → check env default → resolve by priority → return Agent or None
|
|
12
|
-
Errors: TypeError (invalid type) | FileNotFoundError (Path doesn't exist)
|
|
13
22
|
"""
|
|
14
23
|
|
|
15
24
|
import os
|
|
16
25
|
from pathlib import Path
|
|
17
26
|
from typing import Union, Optional
|
|
18
27
|
|
|
19
|
-
from .prompts import get_trust_prompt
|
|
20
28
|
from .tools import get_trust_verification_tools
|
|
29
|
+
from .fast_rules import parse_policy
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Path to trust policy files (at repo root: prompts/trust/)
|
|
33
|
+
PROMPTS_DIR = Path(__file__).parent.parent.parent.parent / "prompts" / "trust"
|
|
21
34
|
|
|
22
35
|
|
|
23
36
|
# Trust level constants
|
|
@@ -45,21 +58,28 @@ def get_default_trust_level() -> Optional[str]:
|
|
|
45
58
|
|
|
46
59
|
def create_trust_agent(trust: Union[str, Path, 'Agent', None], api_key: Optional[str] = None, model: str = "gpt-5-mini") -> Optional['Agent']:
|
|
47
60
|
"""
|
|
48
|
-
|
|
61
|
+
DEPRECATED: Use TrustAgent instead.
|
|
62
|
+
|
|
63
|
+
>>> from connectonion.network.trust import TrustAgent
|
|
64
|
+
>>> trust = TrustAgent("careful")
|
|
65
|
+
>>> trust.should_allow("client-123")
|
|
66
|
+
|
|
67
|
+
This function returns a regular Agent, which lacks TrustAgent methods
|
|
68
|
+
like should_allow(), promote_to_contact(), etc.
|
|
49
69
|
|
|
50
70
|
Args:
|
|
51
|
-
trust: Trust configuration
|
|
52
|
-
- None: Check CONNECTONION_TRUST env, else return None
|
|
53
|
-
- Agent: Return as-is (must have tools)
|
|
54
|
-
- Path: Read file as policy
|
|
55
|
-
- str: Resolved by priority:
|
|
56
|
-
1. Trust level ("open", "careful", "strict")
|
|
57
|
-
2. File path (if file exists)
|
|
58
|
-
3. Inline policy text
|
|
71
|
+
trust: Trust configuration (see TrustAgent for new API)
|
|
59
72
|
|
|
60
73
|
Returns:
|
|
61
74
|
Agent configured for trust verification, or None
|
|
62
75
|
"""
|
|
76
|
+
import warnings
|
|
77
|
+
warnings.warn(
|
|
78
|
+
"create_trust_agent() is deprecated. Use TrustAgent instead: "
|
|
79
|
+
"from connectonion.network.trust import TrustAgent; trust = TrustAgent('careful')",
|
|
80
|
+
DeprecationWarning,
|
|
81
|
+
stacklevel=2
|
|
82
|
+
)
|
|
63
83
|
from ...core.agent import Agent # Import here to avoid circular dependency
|
|
64
84
|
|
|
65
85
|
# If None, check for environment default
|
|
@@ -95,13 +115,20 @@ def create_trust_agent(trust: Union[str, Path, 'Agent', None], api_key: Optional
|
|
|
95
115
|
# Handle string: trust level > file path > inline policy
|
|
96
116
|
if isinstance(trust, str):
|
|
97
117
|
if trust.lower() in TRUST_LEVELS:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
# Load from prompts/trust/{level}.md
|
|
119
|
+
policy_path = PROMPTS_DIR / f"{trust.lower()}.md"
|
|
120
|
+
if policy_path.exists():
|
|
121
|
+
policy_text = policy_path.read_text(encoding='utf-8')
|
|
122
|
+
config, markdown_body = parse_policy(policy_text)
|
|
123
|
+
return Agent(
|
|
124
|
+
name=f"trust_agent_{trust.lower()}",
|
|
125
|
+
tools=trust_tools,
|
|
126
|
+
system_prompt=markdown_body,
|
|
127
|
+
api_key=api_key,
|
|
128
|
+
model=model
|
|
129
|
+
)
|
|
130
|
+
# Fallback if file doesn't exist
|
|
131
|
+
raise FileNotFoundError(f"Trust policy file not found: {policy_path}")
|
|
105
132
|
|
|
106
133
|
path = Path(trust)
|
|
107
134
|
if path.exists() and path.is_file():
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parse YAML config from trust policy files and execute fast rules.
|
|
3
|
+
|
|
4
|
+
Config format:
|
|
5
|
+
allow: [whitelisted, contact] # Who has access
|
|
6
|
+
deny: [blocked] # Who is blocked
|
|
7
|
+
onboard: # How strangers become contacts
|
|
8
|
+
invite_code: [CODE1, CODE2]
|
|
9
|
+
payment: 10
|
|
10
|
+
default: deny # allow | deny | ask
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from .tools import is_whitelisted, is_blocked, is_contact, promote_to_contact
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_policy(policy_text: str) -> tuple[dict, str]:
|
|
19
|
+
"""
|
|
20
|
+
Parse YAML frontmatter from markdown policy file.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
(config_dict, markdown_body)
|
|
24
|
+
"""
|
|
25
|
+
if not policy_text.startswith('---'):
|
|
26
|
+
return {}, policy_text
|
|
27
|
+
|
|
28
|
+
end = policy_text.find('---', 3)
|
|
29
|
+
if end == -1:
|
|
30
|
+
return {}, policy_text
|
|
31
|
+
|
|
32
|
+
yaml_content = policy_text[3:end].strip()
|
|
33
|
+
markdown_body = policy_text[end + 3:].strip()
|
|
34
|
+
|
|
35
|
+
config = yaml.safe_load(yaml_content) or {}
|
|
36
|
+
return config, markdown_body
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def evaluate_request(config: dict, client_id: str, request: dict) -> Optional[str]:
|
|
40
|
+
"""
|
|
41
|
+
Evaluate request using fast rules (no LLM).
|
|
42
|
+
|
|
43
|
+
Config format:
|
|
44
|
+
allow: [whitelisted, contact] # Who has access
|
|
45
|
+
deny: [blocked] # Who is blocked
|
|
46
|
+
onboard: # How strangers become contacts
|
|
47
|
+
invite_code: [CODE1]
|
|
48
|
+
payment: 10
|
|
49
|
+
default: deny # allow | deny | ask
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
config: Parsed YAML config
|
|
53
|
+
client_id: The client making request
|
|
54
|
+
request: Request data (may contain invite_code, payment, etc.)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
'allow', 'deny', or None (needs LLM)
|
|
58
|
+
"""
|
|
59
|
+
# 1. Check deny list first (blocked users)
|
|
60
|
+
deny_list = config.get('deny', ['blocked'])
|
|
61
|
+
for condition in deny_list:
|
|
62
|
+
if condition == 'blocked' and is_blocked(client_id):
|
|
63
|
+
return 'deny'
|
|
64
|
+
|
|
65
|
+
# 2. Check allow list (whitelisted, contacts)
|
|
66
|
+
allow_list = config.get('allow', [])
|
|
67
|
+
for condition in allow_list:
|
|
68
|
+
if condition == 'whitelisted' and is_whitelisted(client_id):
|
|
69
|
+
return 'allow'
|
|
70
|
+
if condition == 'contact' and is_contact(client_id):
|
|
71
|
+
return 'allow'
|
|
72
|
+
|
|
73
|
+
# 3. Try onboarding (stranger → contact)
|
|
74
|
+
onboard = config.get('onboard', {})
|
|
75
|
+
|
|
76
|
+
# Check invite code
|
|
77
|
+
valid_codes = onboard.get('invite_code', [])
|
|
78
|
+
request_code = request.get('invite_code')
|
|
79
|
+
if request_code and request_code in valid_codes:
|
|
80
|
+
promote_to_contact(client_id)
|
|
81
|
+
return 'allow'
|
|
82
|
+
|
|
83
|
+
# Check payment
|
|
84
|
+
required_payment = onboard.get('payment')
|
|
85
|
+
request_payment = request.get('payment', 0)
|
|
86
|
+
if required_payment and request_payment >= required_payment:
|
|
87
|
+
promote_to_contact(client_id)
|
|
88
|
+
return 'allow'
|
|
89
|
+
|
|
90
|
+
# 4. Default action for strangers without onboarding
|
|
91
|
+
default = config.get('default', 'deny')
|
|
92
|
+
|
|
93
|
+
if default == 'allow':
|
|
94
|
+
return 'allow'
|
|
95
|
+
elif default == 'deny':
|
|
96
|
+
return 'deny'
|
|
97
|
+
elif default == 'ask':
|
|
98
|
+
return None # Needs LLM evaluation
|
|
99
|
+
|
|
100
|
+
return 'deny' # Safe fallback
|