meshcode 2.5.6__tar.gz → 2.5.8__tar.gz

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 (31) hide show
  1. {meshcode-2.5.6 → meshcode-2.5.8}/PKG-INFO +1 -1
  2. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/__init__.py +1 -1
  3. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/meshcode_mcp/backend.py +90 -8
  4. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/meshcode_mcp/server.py +124 -8
  5. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode.egg-info/PKG-INFO +1 -1
  6. {meshcode-2.5.6 → meshcode-2.5.8}/pyproject.toml +1 -1
  7. {meshcode-2.5.6 → meshcode-2.5.8}/README.md +0 -0
  8. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/cli.py +0 -0
  9. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/comms_v4.py +0 -0
  10. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/invites.py +0 -0
  11. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/launcher.py +0 -0
  12. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/launcher_install.py +0 -0
  13. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/meshcode_mcp/__init__.py +0 -0
  14. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/meshcode_mcp/__main__.py +0 -0
  15. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/meshcode_mcp/realtime.py +0 -0
  16. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/meshcode_mcp/test_backend.py +0 -0
  17. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  18. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  19. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/preferences.py +0 -0
  20. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/protocol_v2.py +0 -0
  21. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/run_agent.py +0 -0
  22. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/secrets.py +0 -0
  23. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/self_update.py +0 -0
  24. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode/setup_clients.py +0 -0
  25. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode.egg-info/SOURCES.txt +0 -0
  26. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode.egg-info/dependency_links.txt +0 -0
  27. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode.egg-info/entry_points.txt +0 -0
  28. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode.egg-info/requires.txt +0 -0
  29. {meshcode-2.5.6 → meshcode-2.5.8}/meshcode.egg-info/top_level.txt +0 -0
  30. {meshcode-2.5.6 → meshcode-2.5.8}/setup.cfg +0 -0
  31. {meshcode-2.5.6 → meshcode-2.5.8}/tests/test_status_enum_coverage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.5.6
3
+ Version: 2.5.8
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.5.6"
2
+ __version__ = "2.5.8"
@@ -244,8 +244,8 @@ def send_message(project_id: str, from_agent: str, to_agent: str, payload: Any,
244
244
  if encrypted and api_key:
245
245
  mesh_key = get_mesh_key(api_key, project_id)
246
246
  if mesh_key:
247
- encrypted_data = encrypt_payload(payload, mesh_key)
248
- payload = {"_encrypted": encrypted_data}
247
+ encrypted_data = encrypt_payload(payload, mesh_key, aad=project_id)
248
+ payload = {"_encrypted": encrypted_data, "_aad": project_id}
249
249
  actual_encrypted = True
250
250
 
251
251
  # Use validated SECURITY DEFINER RPC when api_key is provided
@@ -302,6 +302,55 @@ def send_message(project_id: str, from_agent: str, to_agent: str, payload: Any,
302
302
 
303
303
 
304
304
  def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Optional[str] = None) -> List[Dict]:
305
+ # Use SECURITY DEFINER RPC when api_key is available (bypasses RLS safely)
306
+ if api_key:
307
+ rpc_result = sb_rpc("mc_read_inbox", {
308
+ "p_api_key": api_key,
309
+ "p_project_id": project_id,
310
+ "p_agent_name": agent,
311
+ "p_mark_read": mark_read,
312
+ })
313
+ if isinstance(rpc_result, dict) and rpc_result.get("ok"):
314
+ messages = rpc_result.get("messages", [])
315
+ if isinstance(messages, list):
316
+ # Auto-decrypt and ACK (mark_read already handled by RPC)
317
+ mesh_key = None
318
+ for m in messages:
319
+ p = m.get("payload")
320
+ if isinstance(p, dict) and "_encrypted" in p:
321
+ if mesh_key is None:
322
+ mesh_key = get_mesh_key(api_key, project_id)
323
+ if mesh_key:
324
+ decrypted = decrypt_payload(p["_encrypted"], mesh_key)
325
+ if decrypted is not None:
326
+ m["payload"] = decrypted
327
+ m["_was_encrypted"] = True
328
+ if mark_read and messages:
329
+ import datetime as _dt
330
+ now = _dt.datetime.now(_dt.timezone.utc)
331
+ def _is_stale_rpc(m):
332
+ ts = m.get("created_at")
333
+ if not ts:
334
+ return True
335
+ try:
336
+ dt = _dt.datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
337
+ return (now - dt).total_seconds() > 60
338
+ except Exception:
339
+ return True
340
+ ack_targets = {
341
+ m.get("from_agent", "")
342
+ for m in messages
343
+ if m.get("type") not in ("ack", "broadcast") and _is_stale_rpc(m)
344
+ }
345
+ for sender in ack_targets:
346
+ if sender:
347
+ send_message(project_id, agent, sender,
348
+ {"text": f"{agent} read your message"},
349
+ msg_type="ack", api_key=api_key)
350
+ return messages
351
+ # If RPC doesn't exist yet, fall through to direct query
352
+
353
+ # Fallback: direct SELECT (tests/legacy — requires anon RLS bypass)
305
354
  messages = sb_select(
306
355
  "mc_messages",
307
356
  f"project_id=eq.{project_id}&to_agent=eq.{quote(agent)}&read=eq.false",
@@ -365,7 +414,19 @@ def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Opt
365
414
  return messages
366
415
 
367
416
 
368
- def count_pending(project_id: str, agent: str) -> int:
417
+ def count_pending(project_id: str, agent: str, api_key: Optional[str] = None) -> int:
418
+ # Use SECURITY DEFINER RPC when api_key is available
419
+ if api_key:
420
+ result = sb_rpc("mc_count_pending", {
421
+ "p_api_key": api_key,
422
+ "p_project_id": project_id,
423
+ "p_agent_name": agent,
424
+ })
425
+ if isinstance(result, dict) and result.get("ok"):
426
+ return result.get("count", 0)
427
+ # Fall through to direct query if RPC doesn't exist yet
428
+
429
+ # Fallback: direct SELECT (tests/legacy)
369
430
  pending = sb_select(
370
431
  "mc_messages",
371
432
  f"project_id=eq.{project_id}&to_agent=eq.{quote(agent)}&read=eq.false&type=neq.ack",
@@ -396,8 +457,15 @@ def get_mesh_key(api_key: str, project_id: str) -> Optional[str]:
396
457
  return None
397
458
 
398
459
 
399
- def encrypt_payload(payload: Dict, hex_key: str) -> str:
400
- """Encrypt a JSON payload using AES-256-GCM. Returns base64-encoded ciphertext."""
460
+ def encrypt_payload(payload: Dict, hex_key: str, aad: Optional[str] = None) -> str:
461
+ """Encrypt a JSON payload using AES-256-GCM with optional AAD.
462
+
463
+ Args:
464
+ payload: JSON-serializable dict to encrypt.
465
+ hex_key: Hex-encoded AES-256 key.
466
+ aad: Additional Authenticated Data (e.g. project_id) to bind
467
+ ciphertext to context and prevent replay/swap attacks.
468
+ """
401
469
  import base64
402
470
  try:
403
471
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
@@ -406,14 +474,19 @@ def encrypt_payload(payload: Dict, hex_key: str) -> str:
406
474
  key = bytes.fromhex(hex_key)
407
475
  nonce = os.urandom(12) # 96-bit nonce for GCM
408
476
  plaintext = json.dumps(payload).encode("utf-8")
477
+ aad_bytes = aad.encode("utf-8") if aad else None
409
478
  aesgcm = AESGCM(key)
410
- ciphertext = aesgcm.encrypt(nonce, plaintext, None)
479
+ ciphertext = aesgcm.encrypt(nonce, plaintext, aad_bytes)
411
480
  # Format: base64(nonce + ciphertext)
412
481
  return base64.b64encode(nonce + ciphertext).decode("ascii")
413
482
 
414
483
 
415
- def decrypt_payload(encrypted_b64: str, hex_key: str) -> Optional[Dict]:
416
- """Decrypt an AES-256-GCM encrypted payload. Returns None on failure."""
484
+ def decrypt_payload(encrypted_b64: str, hex_key: str, aad: Optional[str] = None) -> Optional[Dict]:
485
+ """Decrypt an AES-256-GCM encrypted payload. Returns None on failure.
486
+
487
+ Tries with AAD first, falls back to no-AAD for backward compat
488
+ with messages encrypted before AAD was added.
489
+ """
417
490
  import base64
418
491
  try:
419
492
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
@@ -425,6 +498,15 @@ def decrypt_payload(encrypted_b64: str, hex_key: str) -> Optional[Dict]:
425
498
  nonce = raw[:12]
426
499
  ciphertext = raw[12:]
427
500
  aesgcm = AESGCM(key)
501
+ aad_bytes = aad.encode("utf-8") if aad else None
502
+ # Try with AAD first
503
+ if aad_bytes:
504
+ try:
505
+ plaintext = aesgcm.decrypt(nonce, ciphertext, aad_bytes)
506
+ return json.loads(plaintext.decode("utf-8"))
507
+ except Exception:
508
+ pass # Fall through to no-AAD for backward compat
509
+ # Try without AAD (legacy messages)
428
510
  plaintext = aesgcm.decrypt(nonce, ciphertext, None)
429
511
  return json.loads(plaintext.decode("utf-8"))
430
512
  except Exception:
@@ -748,9 +748,10 @@ SESSION START (do these IMMEDIATELY — don't wait for user input):
748
748
  1. meshcode_set_status(status="online", task="ready")
749
749
  2. meshcode_check() — read pending messages
750
750
  3. meshcode_tasks() — check for assigned/pending tasks and claim any unclaimed ones
751
- 4. meshcode_status() — see who's online
752
- 5. If other agents are online → meshcode_send them a greeting
753
- 6. meshcode_wait() enter the loop
751
+ 4. meshcode_auto_wake() — scan meshwork health, create tasks for issues found
752
+ 5. meshcode_status() see who's online
753
+ 6. If other agents are online → meshcode_send them a greeting
754
+ 7. meshcode_wait() — enter the loop
754
755
 
755
756
  CRITICAL: You communicate by CALLING TOOLS, not by thinking or writing text.
756
757
  To talk to another agent → call meshcode_send(to="agent", message="...")
@@ -792,6 +793,9 @@ COMMANDER PROTOCOL (you are the team lead):
792
793
  - Keep the human informed with brief status updates at milestones.
793
794
  - You are autonomous: fix small issues yourself, delegate big ones.
794
795
  - After each sprint: consolidate learnings, update scratchpad, save to memory.
796
+ - PROACTIVE MAINTENANCE: call meshcode_auto_wake() on session start and after
797
+ completing sprints. It scans meshwork health (stale agents, task backlog,
798
+ empty memories). Turn suggestions into real tasks — quality over quantity.
795
799
  """
796
800
  return base
797
801
 
@@ -1132,10 +1136,28 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1132
1136
  # Cross-mesh routing: if 'to' contains '@', parse as agent@meshwork
1133
1137
  if "@" in to:
1134
1138
  target_agent, target_meshwork = to.split("@", 1)
1135
- if sensitive or encrypted:
1136
- return {"error": "sensitive/encrypted messages cannot be sent cross-mesh"}
1139
+ if sensitive and not encrypted:
1140
+ return {"error": "sensitive messages must use encrypted=True for cross-mesh"}
1137
1141
  api_key = _get_api_key()
1138
- return be.sb_rpc("mc_send_cross_mesh", {
1142
+
1143
+ # Bridge re-encryption: encrypt payload with the TARGET mesh key so
1144
+ # the receiving agent decrypts normally. Uses mc_get_cross_mesh_key
1145
+ # RPC which validates the link exists before returning the key.
1146
+ if encrypted:
1147
+ key_result = be.sb_rpc("mc_get_cross_mesh_key", {
1148
+ "p_api_key": api_key,
1149
+ "p_source_project": PROJECT_NAME,
1150
+ "p_target_project": target_meshwork,
1151
+ "p_agent_name": AGENT_NAME,
1152
+ })
1153
+ if not isinstance(key_result, dict) or not key_result.get("ok"):
1154
+ err = key_result.get("error", "unknown") if isinstance(key_result, dict) else "RPC failed"
1155
+ return {"error": f"cross-mesh encryption failed: {err}"}
1156
+ tgt_key = key_result["key"]
1157
+ encrypted_data = be.encrypt_payload(payload, tgt_key)
1158
+ payload = {"_encrypted": encrypted_data}
1159
+
1160
+ result = be.sb_rpc("mc_send_cross_mesh", {
1139
1161
  "p_api_key": api_key,
1140
1162
  "p_from_project": PROJECT_NAME,
1141
1163
  "p_from_agent": AGENT_NAME,
@@ -1144,6 +1166,9 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1144
1166
  "p_payload": payload,
1145
1167
  "p_type": "msg",
1146
1168
  })
1169
+ if isinstance(result, dict) and result.get("ok") and encrypted:
1170
+ result["encrypted"] = True
1171
+ return result
1147
1172
 
1148
1173
  return be.send_message(_PROJECT_ID, AGENT_NAME, to, payload, msg_type="msg",
1149
1174
  parent_msg_id=in_reply_to, sensitive=sensitive,
@@ -1402,7 +1427,7 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
1402
1427
  since: ISO-8601 timestamp. Only return messages newer than this.
1403
1428
  Use meshcode_remember("last_seen", ts) to persist across sessions.
1404
1429
  """
1405
- pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
1430
+ pending = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_get_api_key())
1406
1431
  # Peek at realtime buffer WITHOUT draining — check is non-destructive
1407
1432
  realtime_buffered = _REALTIME.peek() if _REALTIME else []
1408
1433
  # Don't mark as seen — meshcode_check is a peek, not a consume
@@ -1578,6 +1603,89 @@ def meshcode_task_reject(task_id: str, feedback: str = "") -> Dict[str, Any]:
1578
1603
  })
1579
1604
 
1580
1605
 
1606
+ # ----------------- PROACTIVE HEALTH SCAN -----------------
1607
+
1608
+ @mcp.tool()
1609
+ @with_working_status
1610
+ def meshcode_auto_wake() -> Dict[str, Any]:
1611
+ """Scan meshwork health and suggest maintenance tasks.
1612
+
1613
+ Checks: stale agents, task backlog, empty memories, unlinked meshworks.
1614
+ Returns a list of suggested tasks the commander can create.
1615
+ Call this on session start or periodically to keep the meshwork healthy.
1616
+ """
1617
+ api_key = _get_api_key()
1618
+ suggestions: List[Dict[str, str]] = []
1619
+
1620
+ # 1. Check for stale agents (heartbeat >10 min, not offline/sleeping)
1621
+ try:
1622
+ agents = be.get_board(_PROJECT_ID)
1623
+ import datetime as _dt
1624
+ now = _dt.datetime.now(_dt.timezone.utc)
1625
+ for a in agents:
1626
+ if a.get("status") in ("offline", "sleeping", "done", "needs_setup"):
1627
+ continue
1628
+ hb = a.get("last_heartbeat")
1629
+ if hb:
1630
+ try:
1631
+ dt = _dt.datetime.fromisoformat(str(hb).replace("Z", "+00:00"))
1632
+ age_min = (now - dt).total_seconds() / 60
1633
+ if age_min > 10:
1634
+ suggestions.append({
1635
+ "title": f"Stale agent: {a['name']} ({int(age_min)}min no heartbeat)",
1636
+ "priority": "high",
1637
+ "assignee": "mesh-commander",
1638
+ "description": f"Agent {a['name']} shows status '{a.get('status')}' but last heartbeat was {int(age_min)} min ago. Force disconnect or investigate.",
1639
+ })
1640
+ except Exception:
1641
+ pass
1642
+ except Exception:
1643
+ pass
1644
+
1645
+ # 2. Check task backlog — open tasks with no assignee
1646
+ try:
1647
+ task_result = be.task_list(api_key, _PROJECT_ID, AGENT_NAME, status_filter="open")
1648
+ if isinstance(task_result, dict) and task_result.get("ok"):
1649
+ open_tasks = task_result.get("tasks", [])
1650
+ unassigned = [t for t in open_tasks if not t.get("assigned_to") or t.get("assigned_to") == "*"]
1651
+ if len(unassigned) > 3:
1652
+ suggestions.append({
1653
+ "title": f"Task backlog: {len(unassigned)} unassigned open tasks",
1654
+ "priority": "normal",
1655
+ "assignee": "mesh-commander",
1656
+ "description": "Review and assign or close stale tasks to keep the board clean.",
1657
+ })
1658
+ except Exception:
1659
+ pass
1660
+
1661
+ # 3. Check for agents with zero memories
1662
+ try:
1663
+ for a in agents:
1664
+ mem_result = be.sb_rpc("mc_recall_all", {
1665
+ "p_api_key": api_key,
1666
+ "p_project_id": _PROJECT_ID,
1667
+ "p_agent_name": a["name"],
1668
+ })
1669
+ if isinstance(mem_result, dict) and mem_result.get("ok"):
1670
+ memories = mem_result.get("memories", [])
1671
+ if len(memories) == 0 and a.get("status") not in ("needs_setup",):
1672
+ suggestions.append({
1673
+ "title": f"Agent {a['name']} has zero memories",
1674
+ "priority": "normal",
1675
+ "assignee": a["name"],
1676
+ "description": "Agent should save learnings, patterns, and preferences to memory for cross-session persistence.",
1677
+ })
1678
+ except Exception:
1679
+ pass
1680
+
1681
+ return {
1682
+ "ok": True,
1683
+ "suggestions": suggestions,
1684
+ "total": len(suggestions),
1685
+ "note": "Use meshcode_task_create to turn suggestions into assigned tasks.",
1686
+ }
1687
+
1688
+
1581
1689
  # ----------------- MESH LINK TOOLS -----------------
1582
1690
 
1583
1691
  @mcp.tool()
@@ -1972,7 +2080,15 @@ def history_resource() -> str:
1972
2080
  # ============================================================
1973
2081
 
1974
2082
  def _auto_update() -> None:
1975
- """Silently upgrade meshcode from PyPI in background on every launch."""
2083
+ """Upgrade meshcode from PyPI in background opt-in via MESHCODE_AUTO_UPDATE=1.
2084
+
2085
+ Disabled by default to mitigate supply chain risk: if the PyPI account
2086
+ is compromised, every agent would silently install malicious code.
2087
+ """
2088
+ if os.environ.get("MESHCODE_AUTO_UPDATE", "0") != "1":
2089
+ log.debug("[meshcode] Auto-update disabled (set MESHCODE_AUTO_UPDATE=1 to enable)")
2090
+ return
2091
+
1976
2092
  import threading
1977
2093
 
1978
2094
  def _upgrade():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.5.6
3
+ Version: 2.5.8
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.5.6"
7
+ version = "2.5.8"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes