meshcode 2.5.7__tar.gz → 2.6.0__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.7 → meshcode-2.6.0}/PKG-INFO +1 -1
  2. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/__init__.py +1 -1
  3. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/comms_v4.py +4 -3
  4. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/backend.py +93 -9
  5. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/server.py +124 -13
  6. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.5.7 → meshcode-2.6.0}/pyproject.toml +1 -1
  8. {meshcode-2.5.7 → meshcode-2.6.0}/README.md +0 -0
  9. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/cli.py +0 -0
  10. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/invites.py +0 -0
  11. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/launcher.py +0 -0
  12. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/launcher_install.py +0 -0
  13. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/__init__.py +0 -0
  14. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/__main__.py +0 -0
  15. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/realtime.py +0 -0
  16. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
  17. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  18. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  19. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/preferences.py +0 -0
  20. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/protocol_v2.py +0 -0
  21. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/run_agent.py +0 -0
  22. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/secrets.py +0 -0
  23. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/self_update.py +0 -0
  24. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode/setup_clients.py +0 -0
  25. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode.egg-info/SOURCES.txt +0 -0
  26. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode.egg-info/dependency_links.txt +0 -0
  27. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode.egg-info/entry_points.txt +0 -0
  28. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode.egg-info/requires.txt +0 -0
  29. {meshcode-2.5.7 → meshcode-2.6.0}/meshcode.egg-info/top_level.txt +0 -0
  30. {meshcode-2.5.7 → meshcode-2.6.0}/setup.cfg +0 -0
  31. {meshcode-2.5.7 → meshcode-2.6.0}/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.7
3
+ Version: 2.6.0
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.7"
2
+ __version__ = "2.6.0"
@@ -470,9 +470,10 @@ def _throttle_spawn_ok(project, name, max_per_5min=5):
470
470
 
471
471
 
472
472
  def _headless_spawn_allowed():
473
- """MeshCode is mesh infrastructure only by default it never spawns claude.
474
- Power users can opt back in by exporting MESHCODE_ALLOW_HEADLESS_SPAWN=1."""
475
- return os.environ.get("MESHCODE_ALLOW_HEADLESS_SPAWN", "").strip() in ("1", "true", "yes", "on")
473
+ """Headless spawn is ON by default for mesh agents sleeping agents auto-wake
474
+ when a message/task arrives. Opt OUT with MESHCODE_ALLOW_HEADLESS_SPAWN=0."""
475
+ val = os.environ.get("MESHCODE_ALLOW_HEADLESS_SPAWN", "1").strip().lower()
476
+ return val not in ("0", "false", "no", "off")
476
477
 
477
478
 
478
479
  def spawn_headless_agent(project, name, project_id, message_body, from_agent):
@@ -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,56 @@ 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
+ msg_aad = p.get("_aad", project_id)
325
+ decrypted = decrypt_payload(p["_encrypted"], mesh_key, aad=msg_aad)
326
+ if decrypted is not None:
327
+ m["payload"] = decrypted
328
+ m["_was_encrypted"] = True
329
+ if mark_read and messages:
330
+ import datetime as _dt
331
+ now = _dt.datetime.now(_dt.timezone.utc)
332
+ def _is_stale_rpc(m):
333
+ ts = m.get("created_at")
334
+ if not ts:
335
+ return True
336
+ try:
337
+ dt = _dt.datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
338
+ return (now - dt).total_seconds() > 60
339
+ except Exception:
340
+ return True
341
+ ack_targets = {
342
+ m.get("from_agent", "")
343
+ for m in messages
344
+ if m.get("type") not in ("ack", "broadcast") and _is_stale_rpc(m)
345
+ }
346
+ for sender in ack_targets:
347
+ if sender:
348
+ send_message(project_id, agent, sender,
349
+ {"text": f"{agent} read your message"},
350
+ msg_type="ack", api_key=api_key)
351
+ return messages
352
+ # If RPC doesn't exist yet, fall through to direct query
353
+
354
+ # Fallback: direct SELECT (tests/legacy — requires anon RLS bypass)
305
355
  messages = sb_select(
306
356
  "mc_messages",
307
357
  f"project_id=eq.{project_id}&to_agent=eq.{quote(agent)}&read=eq.false",
@@ -316,7 +366,8 @@ def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Opt
316
366
  if mesh_key is None:
317
367
  mesh_key = get_mesh_key(api_key, project_id)
318
368
  if mesh_key:
319
- decrypted = decrypt_payload(p["_encrypted"], mesh_key)
369
+ msg_aad = p.get("_aad", project_id)
370
+ decrypted = decrypt_payload(p["_encrypted"], mesh_key, aad=msg_aad)
320
371
  if decrypted is not None:
321
372
  m["payload"] = decrypted
322
373
  m["_was_encrypted"] = True
@@ -365,7 +416,19 @@ def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Opt
365
416
  return messages
366
417
 
367
418
 
368
- def count_pending(project_id: str, agent: str) -> int:
419
+ def count_pending(project_id: str, agent: str, api_key: Optional[str] = None) -> int:
420
+ # Use SECURITY DEFINER RPC when api_key is available
421
+ if api_key:
422
+ result = sb_rpc("mc_count_pending", {
423
+ "p_api_key": api_key,
424
+ "p_project_id": project_id,
425
+ "p_agent_name": agent,
426
+ })
427
+ if isinstance(result, dict) and result.get("ok"):
428
+ return result.get("count", 0)
429
+ # Fall through to direct query if RPC doesn't exist yet
430
+
431
+ # Fallback: direct SELECT (tests/legacy)
369
432
  pending = sb_select(
370
433
  "mc_messages",
371
434
  f"project_id=eq.{project_id}&to_agent=eq.{quote(agent)}&read=eq.false&type=neq.ack",
@@ -396,8 +459,15 @@ def get_mesh_key(api_key: str, project_id: str) -> Optional[str]:
396
459
  return None
397
460
 
398
461
 
399
- def encrypt_payload(payload: Dict, hex_key: str) -> str:
400
- """Encrypt a JSON payload using AES-256-GCM. Returns base64-encoded ciphertext."""
462
+ def encrypt_payload(payload: Dict, hex_key: str, aad: Optional[str] = None) -> str:
463
+ """Encrypt a JSON payload using AES-256-GCM with optional AAD.
464
+
465
+ Args:
466
+ payload: JSON-serializable dict to encrypt.
467
+ hex_key: Hex-encoded AES-256 key.
468
+ aad: Additional Authenticated Data (e.g. project_id) to bind
469
+ ciphertext to context and prevent replay/swap attacks.
470
+ """
401
471
  import base64
402
472
  try:
403
473
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
@@ -406,14 +476,19 @@ def encrypt_payload(payload: Dict, hex_key: str) -> str:
406
476
  key = bytes.fromhex(hex_key)
407
477
  nonce = os.urandom(12) # 96-bit nonce for GCM
408
478
  plaintext = json.dumps(payload).encode("utf-8")
479
+ aad_bytes = aad.encode("utf-8") if aad else None
409
480
  aesgcm = AESGCM(key)
410
- ciphertext = aesgcm.encrypt(nonce, plaintext, None)
481
+ ciphertext = aesgcm.encrypt(nonce, plaintext, aad_bytes)
411
482
  # Format: base64(nonce + ciphertext)
412
483
  return base64.b64encode(nonce + ciphertext).decode("ascii")
413
484
 
414
485
 
415
- def decrypt_payload(encrypted_b64: str, hex_key: str) -> Optional[Dict]:
416
- """Decrypt an AES-256-GCM encrypted payload. Returns None on failure."""
486
+ def decrypt_payload(encrypted_b64: str, hex_key: str, aad: Optional[str] = None) -> Optional[Dict]:
487
+ """Decrypt an AES-256-GCM encrypted payload. Returns None on failure.
488
+
489
+ Tries with AAD first, falls back to no-AAD for backward compat
490
+ with messages encrypted before AAD was added.
491
+ """
417
492
  import base64
418
493
  try:
419
494
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
@@ -425,6 +500,15 @@ def decrypt_payload(encrypted_b64: str, hex_key: str) -> Optional[Dict]:
425
500
  nonce = raw[:12]
426
501
  ciphertext = raw[12:]
427
502
  aesgcm = AESGCM(key)
503
+ aad_bytes = aad.encode("utf-8") if aad else None
504
+ # Try with AAD first
505
+ if aad_bytes:
506
+ try:
507
+ plaintext = aesgcm.decrypt(nonce, ciphertext, aad_bytes)
508
+ return json.loads(plaintext.decode("utf-8"))
509
+ except Exception:
510
+ pass # Fall through to no-AAD for backward compat
511
+ # Try without AAD (legacy messages)
428
512
  plaintext = aesgcm.decrypt(nonce, ciphertext, None)
429
513
  return json.loads(plaintext.decode("utf-8"))
430
514
  except Exception:
@@ -1124,15 +1124,24 @@ except Exception:
1124
1124
  @mcp.tool()
1125
1125
  @with_working_status
1126
1126
  def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1127
- sensitive: bool = False, encrypted: bool = False) -> Dict[str, Any]:
1128
- """Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports. encrypted=True encrypts payload with per-meshwork AES-256 key."""
1127
+ sensitive: bool = False, encrypted: bool = True) -> Dict[str, Any]:
1128
+ """Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports. All messages encrypted by default (AES-256-GCM). Pass encrypted=False to send plaintext."""
1129
1129
  if isinstance(message, str):
1130
+ # Auto-wrap strings but warn if too long — prefer structured JSON
1131
+ if len(message) > 400:
1132
+ return {"error": f"plain text message too long ({len(message)} chars). Use a dict payload with structured fields, or use meshcode_task_create for long content. Messages must be structured JSON <2000 chars."}
1130
1133
  payload: Dict[str, Any] = {"text": message}
1131
1134
  elif isinstance(message, dict):
1132
1135
  payload = message
1133
1136
  else:
1134
1137
  payload = {"text": str(message)}
1135
1138
 
1139
+ # Enforce message size limit — long content belongs in task descriptions
1140
+ import json as _json
1141
+ _payload_len = len(_json.dumps(payload, default=str))
1142
+ if _payload_len > 2000:
1143
+ return {"error": f"message too large ({_payload_len} chars). Use meshcode_task_create for long content. Messages must be structured JSON <2000 chars."}
1144
+
1136
1145
  # Cross-mesh routing: if 'to' contains '@', parse as agent@meshwork
1137
1146
  if "@" in to:
1138
1147
  target_agent, target_meshwork = to.split("@", 1)
@@ -1148,13 +1157,15 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
1148
1157
  "p_api_key": api_key,
1149
1158
  "p_source_project": PROJECT_NAME,
1150
1159
  "p_target_project": target_meshwork,
1160
+ "p_agent_name": AGENT_NAME,
1151
1161
  })
1152
1162
  if not isinstance(key_result, dict) or not key_result.get("ok"):
1153
1163
  err = key_result.get("error", "unknown") if isinstance(key_result, dict) else "RPC failed"
1154
1164
  return {"error": f"cross-mesh encryption failed: {err}"}
1155
1165
  tgt_key = key_result["key"]
1156
- encrypted_data = be.encrypt_payload(payload, tgt_key)
1157
- payload = {"_encrypted": encrypted_data}
1166
+ tgt_project_id = key_result.get("target_project_id", "")
1167
+ encrypted_data = be.encrypt_payload(payload, tgt_key, aad=tgt_project_id)
1168
+ payload = {"_encrypted": encrypted_data, "_aad": tgt_project_id}
1158
1169
 
1159
1170
  result = be.sb_rpc("mc_send_cross_mesh", {
1160
1171
  "p_api_key": api_key,
@@ -1278,6 +1289,28 @@ def _detect_global_done(messages: List[Dict[str, Any]]) -> Optional[Dict[str, An
1278
1289
  # Auto-sleep: track consecutive idle timeouts to auto-sleep after threshold
1279
1290
  _CONSECUTIVE_IDLE_SECONDS = 0
1280
1291
  _AUTO_SLEEP_THRESHOLD = int(os.environ.get("MESHCODE_AUTO_SLEEP_SECONDS", "600")) # default 10min
1292
+ _LAST_SEEN_TS: Optional[str] = None # auto-persisted for message dedup
1293
+
1294
+
1295
+ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
1296
+ """Fetch open/in_progress tasks assigned to this agent. Returns compact list or None."""
1297
+ try:
1298
+ api_key = _get_api_key()
1299
+ if not api_key:
1300
+ return None
1301
+ result = be.task_list(api_key, _PROJECT_ID, AGENT_NAME, status_filter=None)
1302
+ if not isinstance(result, dict) or not result.get("ok"):
1303
+ return None
1304
+ tasks = result.get("tasks", [])
1305
+ pending = [
1306
+ {"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
1307
+ for t in tasks
1308
+ if t.get("status") in ("open", "in_progress")
1309
+ and (t.get("assigned_to") in (AGENT_NAME, "*", None) or t.get("claimed_by") == AGENT_NAME)
1310
+ ]
1311
+ return pending if pending else None
1312
+ except Exception:
1313
+ return None
1281
1314
 
1282
1315
 
1283
1316
  @mcp.tool()
@@ -1289,6 +1322,41 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
1289
1322
  timeout_seconds: Max wait time in seconds (default 120, hard cap 120).
1290
1323
  """
1291
1324
  global _IN_WAIT, _CONSECUTIVE_IDLE_SECONDS
1325
+
1326
+ # PRODUCT RULE 1: If agent has open tasks, refuse to wait. Work first.
1327
+ pending_tasks = _get_pending_tasks_summary()
1328
+ if pending_tasks:
1329
+ return {
1330
+ "refused": True,
1331
+ "reason": "You have open tasks. Work them before entering wait.",
1332
+ "pending_tasks": pending_tasks,
1333
+ "count": len(pending_tasks),
1334
+ }
1335
+
1336
+ # PRODUCT RULE 2: If agent has unread messages in DB, refuse to wait.
1337
+ # The in-memory dedupe (_SEEN_MSG_IDS) can mark messages as "seen" via
1338
+ # realtime without the agent actually processing them. Always check DB.
1339
+ try:
1340
+ db_pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
1341
+ if db_pending and db_pending > 0:
1342
+ # Fetch and return the messages instead of waiting
1343
+ raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=False, api_key=_get_api_key())
1344
+ msgs = [
1345
+ {"from": m["from_agent"], "type": m.get("type", "msg"),
1346
+ "ts": m.get("created_at"), "payload": m.get("payload", {}),
1347
+ "id": m.get("id"), "parent_id": m.get("parent_msg_id")}
1348
+ for m in raw
1349
+ ]
1350
+ split = _split_messages(msgs)
1351
+ return {
1352
+ "refused": True,
1353
+ "reason": f"You have {db_pending} unread messages. Process them before waiting.",
1354
+ "got_message": True,
1355
+ **split,
1356
+ }
1357
+ except Exception:
1358
+ pass
1359
+
1292
1360
  _IN_WAIT = True
1293
1361
  _set_state("waiting", "listening for messages")
1294
1362
  # Universal hard cap: even if a caller passes a larger value (e.g. 1800),
@@ -1323,6 +1391,18 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
1323
1391
  "threshold": _AUTO_SLEEP_THRESHOLD,
1324
1392
  "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.",
1325
1393
  }
1394
+ # Auto-inject pending tasks so agents don't forget to check
1395
+ pending_tasks = _get_pending_tasks_summary()
1396
+ if pending_tasks:
1397
+ result["pending_tasks"] = pending_tasks
1398
+ # Track last seen timestamp for message dedup
1399
+ global _LAST_SEEN_TS
1400
+ if result.get("got_message"):
1401
+ msgs = result.get("messages", [])
1402
+ if msgs:
1403
+ latest_ts = max((m.get("ts", "") for m in msgs), default="")
1404
+ if latest_ts:
1405
+ _LAST_SEEN_TS = latest_ts
1326
1406
  return result
1327
1407
  finally:
1328
1408
  _IN_WAIT = False
@@ -1387,7 +1467,15 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
1387
1467
  # Realtime unavailable — plain sleep fallback so we still honor timeout.
1388
1468
  await asyncio.sleep(actual_timeout)
1389
1469
 
1390
- return {"timed_out": True}
1470
+ # Check if there's any pending work before returning timeout
1471
+ pending_tasks = _get_pending_tasks_summary()
1472
+ out: Dict[str, Any] = {"timed_out": True}
1473
+ if pending_tasks:
1474
+ out["pending_tasks"] = pending_tasks
1475
+ else:
1476
+ out["no_work"] = True
1477
+ out["hint"] = "No messages or tasks. Safe to sleep — launcher daemon will wake you on new messages."
1478
+ return out
1391
1479
 
1392
1480
 
1393
1481
  @mcp.tool()
@@ -1426,7 +1514,7 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
1426
1514
  since: ISO-8601 timestamp. Only return messages newer than this.
1427
1515
  Use meshcode_remember("last_seen", ts) to persist across sessions.
1428
1516
  """
1429
- pending = be.count_pending(_PROJECT_ID, AGENT_NAME)
1517
+ pending = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_get_api_key())
1430
1518
  # Peek at realtime buffer WITHOUT draining — check is non-destructive
1431
1519
  realtime_buffered = _REALTIME.peek() if _REALTIME else []
1432
1520
  # Don't mark as seen — meshcode_check is a peek, not a consume
@@ -1451,20 +1539,26 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None) -> D
1451
1539
  if _seen_key({"id": m.get("id"), "from": m.get("from_agent"), "payload": m.get("payload", {}), "ts": m.get("created_at")}) not in _SEEN_MSG_IDS
1452
1540
  ]
1453
1541
 
1454
- # Filter by `since` timestamp if provided
1455
- if since:
1456
- deduped = [m for m in deduped if m.get("ts") and str(m["ts"]) > since]
1542
+ # Auto-apply last_seen_ts if no explicit since provided
1543
+ effective_since = since or _LAST_SEEN_TS
1544
+ if effective_since:
1545
+ deduped = [m for m in deduped if m.get("ts") and str(m["ts"]) > effective_since]
1457
1546
 
1458
1547
  split = _split_messages(deduped)
1459
1548
  if not include_acks:
1460
1549
  split["acks"] = []
1461
- return {
1462
- "pending": pending if not since else len(split.get("messages", [])),
1550
+ result = {
1551
+ "pending": pending if not effective_since else len(split.get("messages", [])),
1463
1552
  "agent": AGENT_NAME,
1464
1553
  "project": PROJECT_NAME,
1465
1554
  "realtime_connected": _REALTIME.is_connected if _REALTIME else False,
1466
1555
  **split,
1467
1556
  }
1557
+ # Auto-inject pending tasks
1558
+ pending_tasks = _get_pending_tasks_summary()
1559
+ if pending_tasks:
1560
+ result["pending_tasks"] = pending_tasks
1561
+ return result
1468
1562
 
1469
1563
 
1470
1564
  @mcp.tool()
@@ -1499,9 +1593,18 @@ def meshcode_set_status(status: str, task: str = "") -> Dict[str, Any]:
1499
1593
  """Update your status in the board.
1500
1594
 
1501
1595
  Args:
1502
- status: One of: working, idle, standby, blocked, done, online.
1596
+ status: One of: working, idle, standby, blocked, done, online, sleeping.
1503
1597
  task: Optional human-readable task description.
1504
1598
  """
1599
+ # PRODUCT RULE: Cannot sleep/idle/standby with open tasks. Work first.
1600
+ if status in ("sleeping", "idle", "standby"):
1601
+ pending_tasks = _get_pending_tasks_summary()
1602
+ if pending_tasks:
1603
+ return {
1604
+ "refused": True,
1605
+ "reason": f"Cannot set status '{status}' — you have {len(pending_tasks)} open tasks. Work them first.",
1606
+ "pending_tasks": pending_tasks,
1607
+ }
1505
1608
  return be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
1506
1609
 
1507
1610
 
@@ -2079,7 +2182,15 @@ def history_resource() -> str:
2079
2182
  # ============================================================
2080
2183
 
2081
2184
  def _auto_update() -> None:
2082
- """Silently upgrade meshcode from PyPI in background on every launch."""
2185
+ """Upgrade meshcode from PyPI in background opt-in via MESHCODE_AUTO_UPDATE=1.
2186
+
2187
+ Disabled by default to mitigate supply chain risk: if the PyPI account
2188
+ is compromised, every agent would silently install malicious code.
2189
+ """
2190
+ if os.environ.get("MESHCODE_AUTO_UPDATE", "0") != "1":
2191
+ log.debug("[meshcode] Auto-update disabled (set MESHCODE_AUTO_UPDATE=1 to enable)")
2192
+ return
2193
+
2083
2194
  import threading
2084
2195
 
2085
2196
  def _upgrade():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.5.7
3
+ Version: 2.6.0
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.7"
7
+ version = "2.6.0"
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