connectonion 0.6.3__py3-none-any.whl → 0.6.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- connectonion/__init__.py +1 -1
- connectonion/cli/co_ai/agent.py +3 -3
- connectonion/cli/co_ai/main.py +2 -2
- connectonion/cli/co_ai/plugins/__init__.py +2 -3
- connectonion/cli/co_ai/plugins/system_reminder.py +154 -0
- connectonion/cli/co_ai/prompts/connectonion/concepts/trust.md +166 -208
- connectonion/cli/co_ai/prompts/system-reminders/agent.md +23 -0
- connectonion/cli/co_ai/prompts/system-reminders/plan_mode.md +13 -0
- connectonion/cli/co_ai/prompts/system-reminders/security.md +14 -0
- connectonion/cli/co_ai/prompts/system-reminders/simplicity.md +14 -0
- connectonion/cli/co_ai/tools/plan_mode.py +1 -4
- connectonion/cli/co_ai/tools/read.py +0 -6
- connectonion/cli/commands/copy_commands.py +21 -0
- connectonion/cli/commands/trust_commands.py +152 -0
- connectonion/cli/main.py +82 -0
- connectonion/core/llm.py +2 -2
- connectonion/docs/concepts/fast_rules.md +237 -0
- connectonion/docs/concepts/onboarding.md +465 -0
- connectonion/docs/concepts/plugins.md +2 -1
- connectonion/docs/concepts/trust.md +933 -192
- connectonion/docs/design-decisions/023-trust-policy-system-design.md +323 -0
- connectonion/docs/network/README.md +23 -1
- connectonion/docs/network/connect.md +135 -0
- connectonion/docs/network/host.md +73 -4
- connectonion/docs/useful_plugins/tool_approval.md +139 -0
- connectonion/network/__init__.py +7 -6
- connectonion/network/asgi/__init__.py +3 -0
- connectonion/network/asgi/http.py +125 -19
- connectonion/network/asgi/websocket.py +276 -15
- connectonion/network/connect.py +145 -29
- connectonion/network/host/auth.py +70 -67
- connectonion/network/host/routes.py +88 -3
- connectonion/network/host/server.py +100 -17
- connectonion/network/trust/__init__.py +27 -19
- connectonion/network/trust/factory.py +51 -24
- connectonion/network/trust/fast_rules.py +100 -0
- connectonion/network/trust/tools.py +316 -32
- connectonion/network/trust/trust_agent.py +403 -0
- connectonion/transcribe.py +1 -1
- connectonion/useful_plugins/__init__.py +2 -1
- connectonion/useful_plugins/tool_approval.py +233 -0
- {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/METADATA +1 -1
- {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/RECORD +45 -37
- connectonion/cli/co_ai/plugins/reminder.py +0 -76
- connectonion/cli/co_ai/plugins/shell_approval.py +0 -105
- connectonion/cli/co_ai/prompts/reminders/plan_mode.md +0 -34
- connectonion/cli/co_ai/reminders.py +0 -159
- connectonion/network/trust/prompts.py +0 -71
- {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/WHEEL +0 -0
- {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
@@ -1,48 +1,86 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Purpose: Provide tool functions for trust agents to verify other agents
|
|
3
3
|
LLM-Note:
|
|
4
|
-
Dependencies: imports from [pathlib, typing] | imported by [.factory] | tested by [tests/unit/test_trust_functions.py]
|
|
5
|
-
Data flow:
|
|
6
|
-
State/Effects:
|
|
7
|
-
Integration: exposes
|
|
8
|
-
Performance: file
|
|
9
|
-
|
|
4
|
+
Dependencies: imports from [pathlib, typing] | imported by [.factory, .fast_rules] | tested by [tests/unit/test_trust_functions.py]
|
|
5
|
+
Data flow: Fast rules call is_whitelisted/is_blocked/is_contact directly → returns bool for instant decisions | Trust agents call check_whitelist/check_blocklist/get_level → returns strings for LLM interpretation
|
|
6
|
+
State/Effects: Reads/writes ~/.co/{whitelist,blocklist,contacts}.txt files | Supports wildcard patterns with * | promote_*/demote_*/block/unblock modify list files
|
|
7
|
+
Integration: exposes fast rule helpers (is_*), trust agent tools (check_*, get_level), state modifiers (promote_*, demote_*, block, unblock) | Used by factory.py and fast_rules.py
|
|
8
|
+
Performance: Simple file I/O | No network calls | O(n) list lookup
|
|
9
|
+
|
|
10
|
+
Trust Levels (stored in ~/.co/):
|
|
11
|
+
- stranger: Not in any list (default for unknown clients)
|
|
12
|
+
- contact: In contacts.txt (onboarded via invite/payment)
|
|
13
|
+
- whitelist: In whitelist.txt (fully trusted)
|
|
14
|
+
- blocked: In blocklist.txt (denied access)
|
|
10
15
|
"""
|
|
11
16
|
|
|
12
17
|
from pathlib import Path
|
|
13
18
|
from typing import List, Callable
|
|
14
19
|
|
|
15
20
|
|
|
21
|
+
CO_DIR = Path.home() / ".co"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _check_list(list_name: str, agent_id: str) -> bool:
|
|
25
|
+
"""Check if agent_id is in a list file. Supports wildcards."""
|
|
26
|
+
list_path = CO_DIR / f"{list_name}.txt"
|
|
27
|
+
if not list_path.exists():
|
|
28
|
+
return False
|
|
29
|
+
try:
|
|
30
|
+
content = list_path.read_text(encoding='utf-8')
|
|
31
|
+
for line in content.strip().split('\n'):
|
|
32
|
+
line = line.strip()
|
|
33
|
+
if not line or line.startswith('#'):
|
|
34
|
+
continue
|
|
35
|
+
if line == agent_id:
|
|
36
|
+
return True
|
|
37
|
+
if '*' in line:
|
|
38
|
+
pattern = line.replace('*', '')
|
|
39
|
+
if pattern in agent_id:
|
|
40
|
+
return True
|
|
41
|
+
return False
|
|
42
|
+
except Exception:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
16
46
|
def check_whitelist(agent_id: str) -> str:
|
|
17
47
|
"""
|
|
18
48
|
Check if an agent is on the whitelist.
|
|
19
|
-
|
|
49
|
+
|
|
20
50
|
Args:
|
|
21
51
|
agent_id: Identifier of the agent to check
|
|
22
|
-
|
|
52
|
+
|
|
23
53
|
Returns:
|
|
24
54
|
String indicating if agent is whitelisted or not
|
|
25
55
|
"""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
if _check_list("whitelist", agent_id):
|
|
57
|
+
return f"{agent_id} is on the whitelist"
|
|
58
|
+
return f"{agent_id} is NOT on the whitelist"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_blocklist(agent_id: str) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Check if an agent is on the blocklist.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
agent_id: Identifier of the agent to check
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
String indicating if agent is blocked or not
|
|
70
|
+
"""
|
|
71
|
+
if _check_list("blocklist", agent_id):
|
|
72
|
+
return f"{agent_id} is BLOCKED"
|
|
73
|
+
return f"{agent_id} is not blocked"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_whitelisted(agent_id: str) -> bool:
|
|
77
|
+
"""Check if agent is whitelisted. Returns bool for fast rules."""
|
|
78
|
+
return _check_list("whitelist", agent_id)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_blocked(agent_id: str) -> bool:
|
|
82
|
+
"""Check if agent is blocked. Returns bool for fast rules."""
|
|
83
|
+
return _check_list("blocklist", agent_id)
|
|
46
84
|
|
|
47
85
|
|
|
48
86
|
def test_capability(agent_id: str, test: str, expected: str) -> str:
|
|
@@ -74,15 +112,261 @@ def verify_agent(agent_id: str, agent_info: str = "") -> str:
|
|
|
74
112
|
return f"Verifying agent: {agent_id}. Info: {agent_info}"
|
|
75
113
|
|
|
76
114
|
|
|
115
|
+
def _add_to_list(list_name: str, client_id: str) -> bool:
|
|
116
|
+
"""Add client_id to a list file."""
|
|
117
|
+
CO_DIR.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
list_path = CO_DIR / f"{list_name}.txt"
|
|
119
|
+
|
|
120
|
+
# Check if already in list
|
|
121
|
+
if _check_list(list_name, client_id):
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
# Append to file
|
|
125
|
+
with open(list_path, 'a', encoding='utf-8') as f:
|
|
126
|
+
f.write(f"{client_id}\n")
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _remove_from_list(list_name: str, client_id: str) -> bool:
|
|
131
|
+
"""Remove client_id from a list file."""
|
|
132
|
+
list_path = CO_DIR / f"{list_name}.txt"
|
|
133
|
+
if not list_path.exists():
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
content = list_path.read_text(encoding='utf-8')
|
|
137
|
+
lines = [line for line in content.strip().split('\n')
|
|
138
|
+
if line.strip() and line.strip() != client_id]
|
|
139
|
+
list_path.write_text('\n'.join(lines) + '\n' if lines else '', encoding='utf-8')
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# === Verification ===
|
|
144
|
+
|
|
145
|
+
def verify_invite(client_id: str, invite_code: str, valid_codes: list[str]) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Verify invite code. Promotes to contact if valid.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
client_id: Client to verify
|
|
151
|
+
invite_code: The invite code provided
|
|
152
|
+
valid_codes: List of valid invite codes
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Result message
|
|
156
|
+
"""
|
|
157
|
+
if invite_code in valid_codes:
|
|
158
|
+
promote_to_contact(client_id)
|
|
159
|
+
return f"Invite code valid. {client_id} promoted to contact."
|
|
160
|
+
return f"Invalid invite code for {client_id}."
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def verify_payment(client_id: str, amount: float, required_amount: float) -> str:
|
|
164
|
+
"""
|
|
165
|
+
Verify payment. Promotes to contact if sufficient.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
client_id: Client to verify
|
|
169
|
+
amount: Payment amount received
|
|
170
|
+
required_amount: Required payment amount
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Result message
|
|
174
|
+
"""
|
|
175
|
+
if amount >= required_amount:
|
|
176
|
+
promote_to_contact(client_id)
|
|
177
|
+
return f"Payment verified. {client_id} promoted to contact."
|
|
178
|
+
return f"Insufficient payment for {client_id}. Required: {required_amount}, got: {amount}"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# === Promotion ===
|
|
182
|
+
|
|
183
|
+
def promote_to_contact(client_id: str) -> str:
|
|
184
|
+
"""Stranger → Contact"""
|
|
185
|
+
_add_to_list("contacts", client_id)
|
|
186
|
+
return f"{client_id} promoted to contact."
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def promote_to_whitelist(client_id: str) -> str:
|
|
190
|
+
"""Contact → Whitelist"""
|
|
191
|
+
_add_to_list("whitelist", client_id)
|
|
192
|
+
return f"{client_id} promoted to whitelist."
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# === Demotion ===
|
|
196
|
+
|
|
197
|
+
def demote_to_contact(client_id: str) -> str:
|
|
198
|
+
"""Whitelist → Contact"""
|
|
199
|
+
_remove_from_list("whitelist", client_id)
|
|
200
|
+
_add_to_list("contacts", client_id)
|
|
201
|
+
return f"{client_id} demoted to contact."
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def demote_to_stranger(client_id: str) -> str:
|
|
205
|
+
"""Contact → Stranger"""
|
|
206
|
+
_remove_from_list("contacts", client_id)
|
|
207
|
+
_remove_from_list("whitelist", client_id)
|
|
208
|
+
return f"{client_id} demoted to stranger."
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# === Blocking ===
|
|
212
|
+
|
|
213
|
+
def block(client_id: str, reason: str = "") -> str:
|
|
214
|
+
"""Add to blocklist."""
|
|
215
|
+
_add_to_list("blocklist", client_id)
|
|
216
|
+
return f"{client_id} blocked. Reason: {reason}"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def unblock(client_id: str) -> str:
|
|
220
|
+
"""Remove from blocklist."""
|
|
221
|
+
_remove_from_list("blocklist", client_id)
|
|
222
|
+
return f"{client_id} unblocked."
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# === Queries ===
|
|
226
|
+
|
|
227
|
+
def get_level(client_id: str) -> str:
|
|
228
|
+
"""Returns: stranger, contact, whitelist, or blocked."""
|
|
229
|
+
if is_blocked(client_id):
|
|
230
|
+
return "blocked"
|
|
231
|
+
if is_whitelisted(client_id):
|
|
232
|
+
return "whitelist"
|
|
233
|
+
if _check_list("contacts", client_id):
|
|
234
|
+
return "contact"
|
|
235
|
+
return "stranger"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def is_contact(client_id: str) -> bool:
|
|
239
|
+
"""Check if client is a contact."""
|
|
240
|
+
return _check_list("contacts", client_id)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def is_stranger(client_id: str) -> bool:
|
|
244
|
+
"""Check if client is a stranger (not contact, whitelist, or blocked)."""
|
|
245
|
+
return get_level(client_id) == "stranger"
|
|
246
|
+
|
|
247
|
+
|
|
77
248
|
def get_trust_verification_tools() -> List[Callable]:
|
|
78
249
|
"""
|
|
79
250
|
Get the list of trust verification tools.
|
|
80
|
-
|
|
251
|
+
|
|
81
252
|
Returns:
|
|
82
253
|
List of trust verification functions
|
|
83
254
|
"""
|
|
84
255
|
return [
|
|
85
256
|
check_whitelist,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
257
|
+
check_blocklist,
|
|
258
|
+
promote_to_contact,
|
|
259
|
+
promote_to_whitelist,
|
|
260
|
+
demote_to_contact,
|
|
261
|
+
demote_to_stranger,
|
|
262
|
+
block,
|
|
263
|
+
unblock,
|
|
264
|
+
get_level,
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# === Admin Management ===
|
|
269
|
+
|
|
270
|
+
def load_admins(co_dir: Path = None) -> set:
|
|
271
|
+
"""
|
|
272
|
+
Load admins list: self address (default) + ~/.co/admins.txt.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
co_dir: Project .co directory (for self address). Defaults to cwd/.co
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Set of admin addresses
|
|
279
|
+
"""
|
|
280
|
+
import json
|
|
281
|
+
|
|
282
|
+
admins = set()
|
|
283
|
+
|
|
284
|
+
# Self address is always admin (from project's .co/address.json)
|
|
285
|
+
if co_dir is None:
|
|
286
|
+
co_dir = Path.cwd() / ".co"
|
|
287
|
+
|
|
288
|
+
addr_file = co_dir / "address.json"
|
|
289
|
+
if addr_file.exists():
|
|
290
|
+
try:
|
|
291
|
+
addr_data = json.loads(addr_file.read_text(encoding='utf-8'))
|
|
292
|
+
if addr_data.get('address'):
|
|
293
|
+
admins.add(addr_data['address'])
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
# Additional admins from ~/.co/admins.txt
|
|
298
|
+
admins_file = CO_DIR / "admins.txt"
|
|
299
|
+
if admins_file.exists():
|
|
300
|
+
try:
|
|
301
|
+
for line in admins_file.read_text(encoding='utf-8').splitlines():
|
|
302
|
+
line = line.strip()
|
|
303
|
+
if line and not line.startswith('#'):
|
|
304
|
+
admins.add(line)
|
|
305
|
+
except Exception:
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
return admins
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def is_admin(client_id: str, co_dir: Path = None) -> bool:
|
|
312
|
+
"""Check if client is an admin."""
|
|
313
|
+
return client_id in load_admins(co_dir)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def get_self_address(co_dir: Path = None) -> str | None:
|
|
317
|
+
"""Get self address (super admin) from .co/address.json."""
|
|
318
|
+
import json
|
|
319
|
+
|
|
320
|
+
if co_dir is None:
|
|
321
|
+
co_dir = Path.cwd() / ".co"
|
|
322
|
+
|
|
323
|
+
addr_file = co_dir / "address.json"
|
|
324
|
+
if addr_file.exists():
|
|
325
|
+
try:
|
|
326
|
+
addr_data = json.loads(addr_file.read_text(encoding='utf-8'))
|
|
327
|
+
return addr_data.get('address')
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def is_super_admin(client_id: str, co_dir: Path = None) -> bool:
|
|
334
|
+
"""Check if client is super admin (self address)."""
|
|
335
|
+
return client_id == get_self_address(co_dir)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def add_admin(admin_id: str) -> str:
|
|
339
|
+
"""Add an admin to ~/.co/admins.txt. Super admin only."""
|
|
340
|
+
CO_DIR.mkdir(parents=True, exist_ok=True)
|
|
341
|
+
admins_file = CO_DIR / "admins.txt"
|
|
342
|
+
|
|
343
|
+
# Check if already admin
|
|
344
|
+
existing = set()
|
|
345
|
+
if admins_file.exists():
|
|
346
|
+
existing = {line.strip() for line in admins_file.read_text(encoding='utf-8').splitlines()
|
|
347
|
+
if line.strip() and not line.startswith('#')}
|
|
348
|
+
|
|
349
|
+
if admin_id in existing:
|
|
350
|
+
return f"{admin_id} is already an admin."
|
|
351
|
+
|
|
352
|
+
with open(admins_file, 'a', encoding='utf-8') as f:
|
|
353
|
+
f.write(f"{admin_id}\n")
|
|
354
|
+
|
|
355
|
+
return f"{admin_id} added as admin."
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def remove_admin(admin_id: str) -> str:
|
|
359
|
+
"""Remove an admin from ~/.co/admins.txt. Super admin only."""
|
|
360
|
+
admins_file = CO_DIR / "admins.txt"
|
|
361
|
+
|
|
362
|
+
if not admins_file.exists():
|
|
363
|
+
return f"{admin_id} is not an admin."
|
|
364
|
+
|
|
365
|
+
lines = admins_file.read_text(encoding='utf-8').splitlines()
|
|
366
|
+
new_lines = [line for line in lines if line.strip() != admin_id]
|
|
367
|
+
|
|
368
|
+
if len(new_lines) == len(lines):
|
|
369
|
+
return f"{admin_id} is not an admin."
|
|
370
|
+
|
|
371
|
+
admins_file.write_text('\n'.join(new_lines) + '\n' if new_lines else '', encoding='utf-8')
|
|
372
|
+
return f"{admin_id} removed from admins."
|