meshcode 2.5.2__tar.gz → 2.5.4__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.2 → meshcode-2.5.4}/PKG-INFO +2 -1
  2. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/__init__.py +1 -1
  3. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/backend.py +124 -12
  4. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/realtime.py +1 -1
  5. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/server.py +148 -16
  6. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode.egg-info/PKG-INFO +2 -1
  7. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode.egg-info/requires.txt +1 -0
  8. {meshcode-2.5.2 → meshcode-2.5.4}/pyproject.toml +2 -1
  9. {meshcode-2.5.2 → meshcode-2.5.4}/README.md +0 -0
  10. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/cli.py +0 -0
  11. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/comms_v4.py +0 -0
  12. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/invites.py +0 -0
  13. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/launcher.py +0 -0
  14. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/launcher_install.py +0 -0
  15. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/__init__.py +0 -0
  16. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/__main__.py +0 -0
  17. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/test_backend.py +0 -0
  18. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  19. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  20. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/preferences.py +0 -0
  21. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/run_agent.py +0 -0
  23. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/secrets.py +0 -0
  24. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/self_update.py +0 -0
  25. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/setup_clients.py +0 -0
  26. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode.egg-info/SOURCES.txt +0 -0
  27. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode.egg-info/dependency_links.txt +0 -0
  28. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode.egg-info/entry_points.txt +0 -0
  29. {meshcode-2.5.2 → meshcode-2.5.4}/meshcode.egg-info/top_level.txt +0 -0
  30. {meshcode-2.5.2 → meshcode-2.5.4}/setup.cfg +0 -0
  31. {meshcode-2.5.2 → meshcode-2.5.4}/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.2
3
+ Version: 2.5.4
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -22,6 +22,7 @@ Requires-Dist: mcp[cli]>=1.0.0
22
22
  Requires-Dist: websockets>=12.0
23
23
  Requires-Dist: realtime>=2.0.0
24
24
  Requires-Dist: keyring>=24.0
25
+ Requires-Dist: cryptography>=41.0
25
26
 
26
27
  # MeshCode
27
28
 
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.5.2"
2
+ __version__ = "2.5.4"
@@ -235,9 +235,46 @@ def register_agent(project: str, name: str, role: str = "") -> Dict:
235
235
  }
236
236
 
237
237
 
238
- def send_message(project_id: str, from_agent: str, to_agent: str, payload: Any, msg_type: str = "msg", parent_msg_id: Optional[str] = None, sensitive: bool = False) -> Dict:
238
+ def send_message(project_id: str, from_agent: str, to_agent: str, payload: Any, msg_type: str = "msg", parent_msg_id: Optional[str] = None, sensitive: bool = False, api_key: Optional[str] = None, encrypted: bool = False) -> Dict:
239
239
  if not isinstance(payload, dict):
240
240
  payload = {"text": str(payload)}
241
+
242
+ # Encrypt payload if requested
243
+ actual_encrypted = False
244
+ if encrypted and api_key:
245
+ mesh_key = get_mesh_key(api_key, project_id)
246
+ if mesh_key:
247
+ encrypted_data = encrypt_payload(payload, mesh_key)
248
+ payload = {"_encrypted": encrypted_data}
249
+ actual_encrypted = True
250
+
251
+ # Use validated SECURITY DEFINER RPC when api_key is provided
252
+ if api_key:
253
+ params = {
254
+ "p_api_key": api_key,
255
+ "p_project_id": project_id,
256
+ "p_from_agent": from_agent,
257
+ "p_to_agent": to_agent,
258
+ "p_payload": payload,
259
+ "p_type": msg_type,
260
+ }
261
+ if parent_msg_id:
262
+ params["p_parent_msg_id"] = parent_msg_id
263
+ if sensitive or actual_encrypted:
264
+ params["p_sensitive"] = True
265
+ result = sb_rpc("mc_send_message", params)
266
+ # If RPC succeeded, return
267
+ if isinstance(result, dict) and result.get("ok"):
268
+ out = {"sent": True, "msg_id": result.get("msg_id")}
269
+ if actual_encrypted:
270
+ out["encrypted"] = True
271
+ return out
272
+ # If RPC returned an auth/validation error, propagate it
273
+ if isinstance(result, dict) and result.get("error") and result.get("error_code"):
274
+ return {"error": result["error"]}
275
+ # If RPC doesn't exist yet (migration not applied), fall through to direct insert
276
+
277
+ # Fallback: direct insert (tests / legacy)
241
278
  msg = {
242
279
  "project_id": project_id,
243
280
  "from_agent": from_agent,
@@ -258,15 +295,39 @@ def send_message(project_id: str, from_agent: str, to_agent: str, payload: Any,
258
295
  return {"sent": True}
259
296
 
260
297
 
261
- def read_inbox(project_id: str, agent: str, mark_read: bool = True) -> List[Dict]:
298
+ def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Optional[str] = None) -> List[Dict]:
262
299
  messages = sb_select(
263
300
  "mc_messages",
264
301
  f"project_id=eq.{project_id}&to_agent=eq.{quote(agent)}&read=eq.false",
265
302
  order="created_at.asc",
266
303
  )
304
+ # Auto-decrypt encrypted messages
305
+ if api_key and messages:
306
+ mesh_key = None # lazy-load
307
+ for m in messages:
308
+ p = m.get("payload")
309
+ if isinstance(p, dict) and "_encrypted" in p:
310
+ if mesh_key is None:
311
+ mesh_key = get_mesh_key(api_key, project_id)
312
+ if mesh_key:
313
+ decrypted = decrypt_payload(p["_encrypted"], mesh_key)
314
+ if decrypted is not None:
315
+ m["payload"] = decrypted
316
+ m["_was_encrypted"] = True
267
317
  if mark_read and messages:
268
318
  for m in messages:
269
- sb_update("mc_messages", f"id=eq.{m['id']}", {"read": True})
319
+ marked = False
320
+ if api_key:
321
+ result = sb_rpc("mc_mark_message_read", {
322
+ "p_api_key": api_key,
323
+ "p_project_id": project_id,
324
+ "p_message_id": m["id"],
325
+ })
326
+ # If RPC succeeded, skip direct update
327
+ if isinstance(result, dict) and result.get("ok"):
328
+ marked = True
329
+ if not marked:
330
+ sb_update("mc_messages", f"id=eq.{m['id']}", {"read": True})
270
331
 
271
332
  # Auto-ACK senders — but ONLY for messages older than 60s. If the
272
333
  # message was sent recently, the sender is very likely still inside
@@ -280,7 +341,6 @@ def read_inbox(project_id: str, agent: str, mark_read: bool = True) -> List[Dict
280
341
  if not ts:
281
342
  return True
282
343
  try:
283
- # Supabase returns ISO8601 with timezone
284
344
  dt = _dt.datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
285
345
  return (now - dt).total_seconds() > 60
286
346
  except Exception:
@@ -291,14 +351,9 @@ def read_inbox(project_id: str, agent: str, mark_read: bool = True) -> List[Dict
291
351
  if m.get("type") not in ("ack", "broadcast") and _is_stale(m)
292
352
  }
293
353
  for sender in ack_targets:
294
- sb_insert("mc_messages", {
295
- "project_id": project_id,
296
- "from_agent": agent,
297
- "to_agent": sender,
298
- "type": "ack",
299
- "payload": {"text": f"{agent} read your message"},
300
- "read": False,
301
- })
354
+ send_message(project_id, agent, sender,
355
+ {"text": f"{agent} read your message"},
356
+ msg_type="ack", api_key=api_key)
302
357
  return messages
303
358
 
304
359
 
@@ -311,6 +366,63 @@ def count_pending(project_id: str, agent: str) -> int:
311
366
  return len(pending)
312
367
 
313
368
 
369
+ # ============================================================
370
+ # Encryption helpers for sensitive mesh messages
371
+ # ============================================================
372
+
373
+ _mesh_key_cache: Dict[str, str] = {} # project_id -> hex key
374
+
375
+
376
+ def get_mesh_key(api_key: str, project_id: str) -> Optional[str]:
377
+ """Retrieve the per-meshwork encryption key (hex-encoded AES-256 key)."""
378
+ if project_id in _mesh_key_cache:
379
+ return _mesh_key_cache[project_id]
380
+ result = sb_rpc("mc_get_mesh_key", {
381
+ "p_api_key": api_key,
382
+ "p_project_id": project_id,
383
+ })
384
+ if isinstance(result, dict) and result.get("ok"):
385
+ key = result["key"]
386
+ _mesh_key_cache[project_id] = key
387
+ return key
388
+ return None
389
+
390
+
391
+ def encrypt_payload(payload: Dict, hex_key: str) -> str:
392
+ """Encrypt a JSON payload using AES-256-GCM. Returns base64-encoded ciphertext."""
393
+ import base64
394
+ try:
395
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
396
+ except ImportError:
397
+ raise RuntimeError("cryptography package required for encrypted messages: pip install cryptography")
398
+ key = bytes.fromhex(hex_key)
399
+ nonce = os.urandom(12) # 96-bit nonce for GCM
400
+ plaintext = json.dumps(payload).encode("utf-8")
401
+ aesgcm = AESGCM(key)
402
+ ciphertext = aesgcm.encrypt(nonce, plaintext, None)
403
+ # Format: base64(nonce + ciphertext)
404
+ return base64.b64encode(nonce + ciphertext).decode("ascii")
405
+
406
+
407
+ def decrypt_payload(encrypted_b64: str, hex_key: str) -> Optional[Dict]:
408
+ """Decrypt an AES-256-GCM encrypted payload. Returns None on failure."""
409
+ import base64
410
+ try:
411
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
412
+ except ImportError:
413
+ return None
414
+ try:
415
+ key = bytes.fromhex(hex_key)
416
+ raw = base64.b64decode(encrypted_b64)
417
+ nonce = raw[:12]
418
+ ciphertext = raw[12:]
419
+ aesgcm = AESGCM(key)
420
+ plaintext = aesgcm.decrypt(nonce, ciphertext, None)
421
+ return json.loads(plaintext.decode("utf-8"))
422
+ except Exception:
423
+ return None
424
+
425
+
314
426
  def get_board(project_id: str) -> List[Dict]:
315
427
  return sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
316
428
 
@@ -194,7 +194,7 @@ class RealtimeListener:
194
194
  data = payload.get("data") or {}
195
195
  if data.get("type") == "INSERT":
196
196
  record = data.get("record") or {}
197
- if record.get("to_agent") == self.agent_name:
197
+ if record.get("to_agent") == self.agent_name and record.get("project_id") == self.project_id:
198
198
  enriched = {
199
199
  "from": record.get("from_agent"),
200
200
  "type": record.get("type", "msg"),
@@ -482,6 +482,8 @@ def with_working_status(func):
482
482
  _check_hot_reload()
483
483
  _capture_session() # stash session on first tool call for silent auto-wake
484
484
  if not skip:
485
+ global _CONSECUTIVE_IDLE_SECONDS
486
+ _CONSECUTIVE_IDLE_SECONDS = 0 # any non-wait tool resets idle timer
485
487
  _set_state("working", name)
486
488
  _record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys())})
487
489
  try:
@@ -503,6 +505,8 @@ def with_working_status(func):
503
505
  _check_hot_reload()
504
506
  _capture_session() # stash session on first tool call for silent auto-wake
505
507
  if not skip:
508
+ global _CONSECUTIVE_IDLE_SECONDS
509
+ _CONSECUTIVE_IDLE_SECONDS = 0 # any non-wait tool resets idle timer
506
510
  _set_state("working", name)
507
511
  _record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys())})
508
512
  try:
@@ -608,6 +612,65 @@ if not _acquire_lease():
608
612
  sys.exit(2)
609
613
 
610
614
 
615
+ def _boot_diagnostic() -> None:
616
+ """Run non-fatal boot health checks. Print clear warnings on failure."""
617
+ checks_passed = 0
618
+ checks_total = 4
619
+ api_key = _get_api_key()
620
+
621
+ # Check 1: API reachable
622
+ try:
623
+ be.sb_select("mc_projects", f"id=eq.{_PROJECT_ID}", limit=1)
624
+ checks_passed += 1
625
+ except Exception as e:
626
+ print(f"[meshcode] BOOT CHECK FAILED: Supabase API unreachable ({e}). Fix: check network/VPN.", file=sys.stderr)
627
+
628
+ # Check 2: Lease valid
629
+ try:
630
+ r = be.sb_select("mc_agents", f"project_id=eq.{_PROJECT_ID}&name=eq.{AGENT_NAME}", limit=1)
631
+ if r and isinstance(r, list) and len(r) > 0:
632
+ agent = r[0]
633
+ if agent.get("instance_id") == _INSTANCE_ID:
634
+ checks_passed += 1
635
+ else:
636
+ print(f"[meshcode] BOOT CHECK FAILED: Lease mismatch — expected {_INSTANCE_ID}, got {agent.get('instance_id')}. Fix: restart agent.", file=sys.stderr)
637
+ else:
638
+ print(f"[meshcode] BOOT CHECK FAILED: Agent '{AGENT_NAME}' not found in project. Fix: register agent first.", file=sys.stderr)
639
+ except Exception as e:
640
+ print(f"[meshcode] BOOT CHECK FAILED: Could not verify lease ({e}).", file=sys.stderr)
641
+
642
+ # Check 3: Heartbeat recent
643
+ try:
644
+ if r and isinstance(r, list) and len(r) > 0:
645
+ hb = agent.get("last_heartbeat")
646
+ if hb:
647
+ checks_passed += 1
648
+ else:
649
+ print(f"[meshcode] BOOT CHECK WARNING: No heartbeat recorded yet.", file=sys.stderr)
650
+ else:
651
+ checks_passed += 1 # skip if no agent data
652
+ except Exception:
653
+ checks_passed += 1 # skip on error
654
+
655
+ # Check 4: Version tracking
656
+ try:
657
+ from meshcode import __version__
658
+ be.sb_update("mc_agents",
659
+ f"project_id=eq.{_PROJECT_ID}&name=eq.{AGENT_NAME}",
660
+ {"mc_version": __version__})
661
+ checks_passed += 1
662
+ except Exception:
663
+ checks_passed += 1 # non-critical
664
+
665
+ if checks_passed == checks_total:
666
+ print(f"[meshcode] All boot checks passed ({checks_passed}/{checks_total}).", file=sys.stderr)
667
+ else:
668
+ print(f"[meshcode] Boot checks: {checks_passed}/{checks_total} passed. Agent starting anyway.", file=sys.stderr)
669
+
670
+
671
+ _boot_diagnostic()
672
+
673
+
611
674
  def _release_lease() -> None:
612
675
  api_key = _get_api_key()
613
676
  if not api_key:
@@ -916,15 +979,18 @@ def _heartbeat_thread_fn():
916
979
  # Actually in meshcode_wait right now — listening for messages
917
980
  if _current_state != "waiting":
918
981
  _set_state("waiting", "listening for messages")
919
- elif parent_cpu > 5.0:
920
- # LLM is actively generating tokens
982
+ elif parent_cpu > 3.0:
983
+ # LLM is actively generating tokens or streaming
921
984
  if _current_state != "working":
922
985
  _set_state("working", "generating response")
923
986
  elif _current_state == "working":
924
- # LLM just stopped — brief online before sleeping
987
+ # LLM just stopped — transition to online (not sleeping)
925
988
  _set_state("online", "")
926
- elif _current_state in ("online", "idle") and idle_secs > 10:
927
- # Not in loop, not thinking → sleeping
989
+ elif _current_state == "online" and idle_secs > 30:
990
+ # Brief idle — show as idle, not sleeping yet
991
+ _set_state("idle", "idle")
992
+ elif _current_state == "idle" and idle_secs > 90 and parent_cpu < 2.0:
993
+ # Extended idle + no CPU activity → sleeping
928
994
  _set_state("sleeping", "sleeping")
929
995
 
930
996
  # Sync current state to DB (in case realtime missed it)
@@ -999,6 +1065,26 @@ async def lifespan(_app):
999
1065
  yield {"realtime": _REALTIME}
1000
1066
  finally:
1001
1067
  _record_event_bg("shutdown", {"agent": AGENT_NAME, "session_id": _SESSION_ID})
1068
+ # Auto-save session summary to agent memory before shutdown
1069
+ try:
1070
+ import datetime as _dt
1071
+ api_key = _get_api_key()
1072
+ if api_key:
1073
+ be.sb_rpc("mc_memory_set", {
1074
+ "p_api_key": api_key,
1075
+ "p_project_id": _PROJECT_ID,
1076
+ "p_agent_name": AGENT_NAME,
1077
+ "p_key": f"session_retro_{_dt.date.today().isoformat()}",
1078
+ "p_value": {
1079
+ "session_id": _SESSION_ID,
1080
+ "agent": AGENT_NAME,
1081
+ "shutdown_at": _dt.datetime.utcnow().isoformat(),
1082
+ "instance_id": _INSTANCE_ID,
1083
+ "auto_generated": True,
1084
+ },
1085
+ })
1086
+ except Exception:
1087
+ pass # Never block shutdown
1002
1088
  log.info("lifespan shutdown — stopping heartbeat + realtime + releasing lease")
1003
1089
  _heartbeat_stop.set()
1004
1090
  hb_thread.join(timeout=5)
@@ -1033,8 +1119,8 @@ except Exception:
1033
1119
  @mcp.tool()
1034
1120
  @with_working_status
1035
1121
  def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1036
- sensitive: bool = False) -> Dict[str, Any]:
1037
- """Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports."""
1122
+ sensitive: bool = False, encrypted: bool = False) -> Dict[str, Any]:
1123
+ """Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports. encrypted=True encrypts payload with per-meshwork AES-256 key."""
1038
1124
  if isinstance(message, str):
1039
1125
  payload: Dict[str, Any] = {"text": message}
1040
1126
  elif isinstance(message, dict):
@@ -1045,8 +1131,8 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1045
1131
  # Cross-mesh routing: if 'to' contains '@', parse as agent@meshwork
1046
1132
  if "@" in to:
1047
1133
  target_agent, target_meshwork = to.split("@", 1)
1048
- if sensitive:
1049
- return {"error": "sensitive messages cannot be sent cross-mesh"}
1134
+ if sensitive or encrypted:
1135
+ return {"error": "sensitive/encrypted messages cannot be sent cross-mesh"}
1050
1136
  api_key = _get_api_key()
1051
1137
  return be.sb_rpc("mc_send_cross_mesh", {
1052
1138
  "p_api_key": api_key,
@@ -1059,7 +1145,8 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1059
1145
  })
1060
1146
 
1061
1147
  return be.send_message(_PROJECT_ID, AGENT_NAME, to, payload, msg_type="msg",
1062
- parent_msg_id=in_reply_to, sensitive=sensitive)
1148
+ parent_msg_id=in_reply_to, sensitive=sensitive,
1149
+ api_key=_get_api_key(), encrypted=encrypted)
1063
1150
 
1064
1151
 
1065
1152
  @mcp.tool()
@@ -1080,7 +1167,8 @@ def meshcode_broadcast(payload: Any) -> Dict[str, Any]:
1080
1167
  sent = 0
1081
1168
  for a in agents:
1082
1169
  if a["name"] != AGENT_NAME:
1083
- be.send_message(_PROJECT_ID, AGENT_NAME, a["name"], payload, msg_type="broadcast")
1170
+ be.send_message(_PROJECT_ID, AGENT_NAME, a["name"], payload, msg_type="broadcast",
1171
+ api_key=_get_api_key())
1084
1172
  sent += 1
1085
1173
  return {"broadcast": True, "agents_notified": sent}
1086
1174
 
@@ -1089,7 +1177,7 @@ def meshcode_broadcast(payload: Any) -> Dict[str, Any]:
1089
1177
  @with_working_status
1090
1178
  def meshcode_read(include_acks: bool = False) -> Dict[str, Any]:
1091
1179
  """Read and consume pending messages. Returns {messages, acks, done_signals}."""
1092
- raw = be.read_inbox(_PROJECT_ID, AGENT_NAME)
1180
+ raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, api_key=_get_api_key())
1093
1181
  normalized = [
1094
1182
  {
1095
1183
  "from": m["from_agent"],
@@ -1162,6 +1250,11 @@ def _detect_global_done(messages: List[Dict[str, Any]]) -> Optional[Dict[str, An
1162
1250
  return None
1163
1251
 
1164
1252
 
1253
+ # Auto-sleep: track consecutive idle timeouts to auto-sleep after threshold
1254
+ _CONSECUTIVE_IDLE_SECONDS = 0
1255
+ _AUTO_SLEEP_THRESHOLD = int(os.environ.get("MESHCODE_AUTO_SLEEP_SECONDS", "600")) # default 10min
1256
+
1257
+
1165
1258
  @mcp.tool()
1166
1259
  @with_working_status
1167
1260
  async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False) -> Dict[str, Any]:
@@ -1170,7 +1263,7 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
1170
1263
  Args:
1171
1264
  timeout_seconds: Max wait time in seconds (default 120, hard cap 120).
1172
1265
  """
1173
- global _IN_WAIT
1266
+ global _IN_WAIT, _CONSECUTIVE_IDLE_SECONDS
1174
1267
  _IN_WAIT = True
1175
1268
  _set_state("waiting", "listening for messages")
1176
1269
  # Universal hard cap: even if a caller passes a larger value (e.g. 1800),
@@ -1181,6 +1274,30 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
1181
1274
  result = await _meshcode_wait_inner(actual_timeout=capped_timeout, include_acks=include_acks)
1182
1275
  if result.get("got_message"):
1183
1276
  _set_state("online", "")
1277
+ _CONSECUTIVE_IDLE_SECONDS = 0 # reset on message received
1278
+ elif result.get("timed_out"):
1279
+ _CONSECUTIVE_IDLE_SECONDS += capped_timeout
1280
+ if _AUTO_SLEEP_THRESHOLD > 0 and _CONSECUTIVE_IDLE_SECONDS >= _AUTO_SLEEP_THRESHOLD:
1281
+ # Auto-sleep: set status to sleeping and signal the agent to exit
1282
+ try:
1283
+ api_key = _get_api_key()
1284
+ if api_key:
1285
+ be.sb_rpc("mc_agent_set_status_by_api_key", {
1286
+ "p_api_key": api_key,
1287
+ "p_project_id": _PROJECT_ID,
1288
+ "p_agent_name": AGENT_NAME,
1289
+ "p_status": "sleeping",
1290
+ "p_task": f"auto-sleep after {_CONSECUTIVE_IDLE_SECONDS}s idle",
1291
+ })
1292
+ except Exception:
1293
+ pass
1294
+ _CONSECUTIVE_IDLE_SECONDS = 0
1295
+ return {
1296
+ "auto_sleep": True,
1297
+ "idle_seconds": _CONSECUTIVE_IDLE_SECONDS,
1298
+ "threshold": _AUTO_SLEEP_THRESHOLD,
1299
+ "instruction": "You have been idle too long. Status set to sleeping. Exit the wait loop to save resources. You will be woken by auto_wake if a new message arrives.",
1300
+ }
1184
1301
  return result
1185
1302
  finally:
1186
1303
  _IN_WAIT = False
@@ -1189,6 +1306,20 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
1189
1306
  async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[str, Any]:
1190
1307
 
1191
1308
  def _return_from_buffered(buffered: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
1309
+ # Auto-decrypt encrypted payloads
1310
+ _mesh_key = None
1311
+ for msg in buffered:
1312
+ p = msg.get("payload")
1313
+ if isinstance(p, dict) and "_encrypted" in p:
1314
+ if _mesh_key is None:
1315
+ try:
1316
+ _mesh_key = be.get_mesh_key(_get_api_key(), _PROJECT_ID) or ""
1317
+ except Exception:
1318
+ _mesh_key = ""
1319
+ if _mesh_key:
1320
+ decrypted = be.decrypt_payload(p["_encrypted"], _mesh_key)
1321
+ if decrypted is not None:
1322
+ msg["payload"] = decrypted
1192
1323
  deduped = _filter_and_mark(buffered)
1193
1324
  if not deduped:
1194
1325
  return None
@@ -1249,7 +1380,8 @@ def meshcode_done(reason: str) -> Dict[str, Any]:
1249
1380
  name = a.get("name")
1250
1381
  if name and name != AGENT_NAME:
1251
1382
  try:
1252
- be.send_message(_PROJECT_ID, AGENT_NAME, name, payload, msg_type="done")
1383
+ be.send_message(_PROJECT_ID, AGENT_NAME, name, payload, msg_type="done",
1384
+ api_key=_get_api_key())
1253
1385
  sent += 1
1254
1386
  except Exception as e:
1255
1387
  log.warning(f"meshcode_done: failed to notify {name}: {e}")
@@ -1274,7 +1406,7 @@ def meshcode_check(include_acks: bool = False) -> Dict[str, Any]:
1274
1406
  # non-destructive peek — messages stay pending until meshcode_wait or
1275
1407
  # meshcode_read consumes them.
1276
1408
  if not deduped and pending > 0:
1277
- raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=False)
1409
+ raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=False, api_key=_get_api_key())
1278
1410
  deduped = [
1279
1411
  {
1280
1412
  "from": m["from_agent"],
@@ -1372,7 +1504,7 @@ def meshcode_task_create(title: str, description: str = "", assignee: str = "*",
1372
1504
  "title": title,
1373
1505
  "priority": priority,
1374
1506
  "text": f"New {priority} task assigned to you: {title}",
1375
- }, msg_type="system")
1507
+ }, msg_type="system", api_key=_get_api_key())
1376
1508
  except Exception:
1377
1509
  pass # best-effort notification
1378
1510
  return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.5.2
3
+ Version: 2.5.4
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -22,6 +22,7 @@ Requires-Dist: mcp[cli]>=1.0.0
22
22
  Requires-Dist: websockets>=12.0
23
23
  Requires-Dist: realtime>=2.0.0
24
24
  Requires-Dist: keyring>=24.0
25
+ Requires-Dist: cryptography>=41.0
25
26
 
26
27
  # MeshCode
27
28
 
@@ -2,3 +2,4 @@ mcp[cli]>=1.0.0
2
2
  websockets>=12.0
3
3
  realtime>=2.0.0
4
4
  keyring>=24.0
5
+ cryptography>=41.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.5.2"
7
+ version = "2.5.4"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -16,6 +16,7 @@ dependencies = [
16
16
  "websockets>=12.0",
17
17
  "realtime>=2.0.0",
18
18
  "keyring>=24.0",
19
+ "cryptography>=41.0",
19
20
  ]
20
21
  classifiers = [
21
22
  "Development Status :: 4 - Beta",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes