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.
- {meshcode-2.5.2 → meshcode-2.5.4}/PKG-INFO +2 -1
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/__init__.py +1 -1
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/backend.py +124 -12
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/realtime.py +1 -1
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/server.py +148 -16
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode.egg-info/PKG-INFO +2 -1
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode.egg-info/requires.txt +1 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/pyproject.toml +2 -1
- {meshcode-2.5.2 → meshcode-2.5.4}/README.md +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/cli.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/comms_v4.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/invites.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/launcher.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/launcher_install.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/preferences.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/run_agent.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/secrets.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/self_update.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode/setup_clients.py +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.5.2 → meshcode-2.5.4}/setup.cfg +0 -0
- {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.
|
|
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
|
+
__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
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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 >
|
|
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 —
|
|
987
|
+
# LLM just stopped — transition to online (not sleeping)
|
|
925
988
|
_set_state("online", "")
|
|
926
|
-
elif _current_state
|
|
927
|
-
#
|
|
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.
|
|
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
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "meshcode"
|
|
7
|
-
version = "2.5.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|