connectonion 0.6.3__py3-none-any.whl → 0.6.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. connectonion/__init__.py +1 -1
  2. connectonion/cli/co_ai/agent.py +3 -3
  3. connectonion/cli/co_ai/main.py +2 -2
  4. connectonion/cli/co_ai/plugins/__init__.py +2 -3
  5. connectonion/cli/co_ai/plugins/system_reminder.py +154 -0
  6. connectonion/cli/co_ai/prompts/connectonion/concepts/trust.md +166 -208
  7. connectonion/cli/co_ai/prompts/system-reminders/agent.md +23 -0
  8. connectonion/cli/co_ai/prompts/system-reminders/plan_mode.md +13 -0
  9. connectonion/cli/co_ai/prompts/system-reminders/security.md +14 -0
  10. connectonion/cli/co_ai/prompts/system-reminders/simplicity.md +14 -0
  11. connectonion/cli/co_ai/tools/plan_mode.py +1 -4
  12. connectonion/cli/co_ai/tools/read.py +0 -6
  13. connectonion/cli/commands/copy_commands.py +21 -0
  14. connectonion/cli/commands/trust_commands.py +152 -0
  15. connectonion/cli/main.py +82 -0
  16. connectonion/core/llm.py +2 -2
  17. connectonion/docs/concepts/fast_rules.md +237 -0
  18. connectonion/docs/concepts/onboarding.md +465 -0
  19. connectonion/docs/concepts/plugins.md +2 -1
  20. connectonion/docs/concepts/trust.md +933 -192
  21. connectonion/docs/design-decisions/023-trust-policy-system-design.md +323 -0
  22. connectonion/docs/network/README.md +23 -1
  23. connectonion/docs/network/connect.md +135 -0
  24. connectonion/docs/network/host.md +73 -4
  25. connectonion/docs/useful_plugins/tool_approval.md +139 -0
  26. connectonion/network/__init__.py +7 -6
  27. connectonion/network/asgi/__init__.py +3 -0
  28. connectonion/network/asgi/http.py +125 -19
  29. connectonion/network/asgi/websocket.py +276 -15
  30. connectonion/network/connect.py +145 -29
  31. connectonion/network/host/auth.py +70 -67
  32. connectonion/network/host/routes.py +88 -3
  33. connectonion/network/host/server.py +100 -17
  34. connectonion/network/trust/__init__.py +27 -19
  35. connectonion/network/trust/factory.py +51 -24
  36. connectonion/network/trust/fast_rules.py +100 -0
  37. connectonion/network/trust/tools.py +316 -32
  38. connectonion/network/trust/trust_agent.py +403 -0
  39. connectonion/transcribe.py +1 -1
  40. connectonion/useful_plugins/__init__.py +2 -1
  41. connectonion/useful_plugins/tool_approval.py +233 -0
  42. {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/METADATA +1 -1
  43. {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/RECORD +45 -37
  44. connectonion/cli/co_ai/plugins/reminder.py +0 -76
  45. connectonion/cli/co_ai/plugins/shell_approval.py +0 -105
  46. connectonion/cli/co_ai/prompts/reminders/plan_mode.md +0 -34
  47. connectonion/cli/co_ai/reminders.py +0 -159
  48. connectonion/network/trust/prompts.py +0 -71
  49. {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/WHEEL +0 -0
  50. {connectonion-0.6.3.dist-info → connectonion-0.6.5.dist-info}/entry_points.txt +0 -0
@@ -1,23 +1,36 @@
1
1
  """
2
- Purpose: Factory for creating trust verification agents with policies.
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
- Create a trust agent based on the trust parameter.
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
- return Agent(
99
- name=f"trust_agent_{trust.lower()}",
100
- tools=trust_tools,
101
- system_prompt=get_trust_prompt(trust.lower()),
102
- api_key=api_key,
103
- model=model
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: create_trust_agent() calls get_trust_verification_tools() returns list of [check_whitelist, test_capability, verify_agent] functions these become tools for trust Agent trust agent calls tools with agent_id functions return descriptive strings AI interprets results per trust policy
6
- State/Effects: check_whitelist() reads ~/.connectonion/trusted.txt file if exists | supports wildcard patterns with * | test_capability() and verify_agent() are pure (no I/O, just return descriptive strings for AI)
7
- Integration: exposes check_whitelist(agent_id), test_capability(agent_id, test, expected), verify_agent(agent_id, agent_info), get_trust_verification_tools() | used by factory.py to equip trust agents with verification capabilities
8
- Performance: file read only for check_whitelist | simple string operations | no network calls
9
- Errors: check_whitelist() catches file read exceptions and returns error string | missing whitelist file returns helpful message | no exceptions raised (errors returned as strings for AI interpretation)
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 directlyreturns bool for instant decisions | Trust agents call check_whitelist/check_blocklist/get_levelreturns 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
- whitelist_path = Path.home() / ".connectonion" / "trusted.txt"
27
- if whitelist_path.exists():
28
- try:
29
- whitelist = whitelist_path.read_text(encoding='utf-8')
30
- lines = whitelist.strip().split('\n')
31
- for line in lines:
32
- line = line.strip()
33
- if not line or line.startswith('#'):
34
- continue
35
- if line == agent_id:
36
- return f"{agent_id} is on the whitelist"
37
- # Simple wildcard support
38
- if '*' in line:
39
- pattern = line.replace('*', '')
40
- if pattern in agent_id:
41
- return f"{agent_id} matches whitelist pattern: {line}"
42
- return f"{agent_id} is NOT on the whitelist"
43
- except Exception as e:
44
- return f"Error reading whitelist: {e}"
45
- return "No whitelist file found at ~/.connectonion/trusted.txt"
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
- test_capability,
87
- verify_agent
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."