meshcode 2.7.0__tar.gz → 2.8.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.
- {meshcode-2.7.0 → meshcode-2.8.0}/PKG-INFO +1 -1
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/__init__.py +1 -1
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/meshcode_mcp/backend.py +44 -3
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/meshcode_mcp/realtime.py +5 -16
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/meshcode_mcp/server.py +41 -43
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.7.0 → meshcode-2.8.0}/pyproject.toml +1 -1
- {meshcode-2.7.0 → meshcode-2.8.0}/README.md +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/cli.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/comms_v4.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/invites.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/launcher.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/launcher_install.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/preferences.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/run_agent.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/secrets.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/self_update.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode/setup_clients.py +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/setup.cfg +0 -0
- {meshcode-2.7.0 → meshcode-2.8.0}/tests/test_status_enum_coverage.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.
|
|
2
|
+
__version__ = "2.8.0"
|
|
@@ -532,7 +532,21 @@ def heartbeat(project_id: str, agent: str) -> Dict:
|
|
|
532
532
|
return result or {}
|
|
533
533
|
|
|
534
534
|
|
|
535
|
-
def set_status(project_id: str, agent: str, status: str, task: str = "") -> Dict:
|
|
535
|
+
def set_status(project_id: str, agent: str, status: str, task: str = "", api_key: Optional[str] = None) -> Dict:
|
|
536
|
+
# Use SECURITY DEFINER RPC when api_key is available (anon PATCH silently fails)
|
|
537
|
+
if api_key:
|
|
538
|
+
rpc_result = sb_rpc("mc_agent_set_status_by_api_key", {
|
|
539
|
+
"p_api_key": api_key,
|
|
540
|
+
"p_project_id": project_id,
|
|
541
|
+
"p_agent_name": agent,
|
|
542
|
+
"p_status": status,
|
|
543
|
+
"p_task": task,
|
|
544
|
+
})
|
|
545
|
+
if isinstance(rpc_result, dict) and rpc_result.get("ok"):
|
|
546
|
+
return {"ok": True, "status": status}
|
|
547
|
+
# Fall through to direct PATCH if RPC doesn't exist yet
|
|
548
|
+
|
|
549
|
+
# Fallback: direct PATCH (may silently fail with anon key if RLS blocks)
|
|
536
550
|
updates = {"status": status, "last_heartbeat": _now_iso()}
|
|
537
551
|
if task:
|
|
538
552
|
updates["task"] = task
|
|
@@ -595,7 +609,20 @@ def record_event(api_key, project_id, agent_name, session_id, event_type, payloa
|
|
|
595
609
|
})
|
|
596
610
|
|
|
597
611
|
|
|
598
|
-
def get_history(project_id: str, limit: int = 20, agent_filter: str = "") -> List[Dict]:
|
|
612
|
+
def get_history(project_id: str, limit: int = 20, agent_filter: str = "", api_key: Optional[str] = None) -> List[Dict]:
|
|
613
|
+
# Use SECURITY DEFINER RPC when api_key is available (bypasses RLS safely)
|
|
614
|
+
if api_key:
|
|
615
|
+
rpc_result = sb_rpc("mc_get_history", {
|
|
616
|
+
"p_api_key": api_key,
|
|
617
|
+
"p_project_id": project_id,
|
|
618
|
+
"p_limit": limit,
|
|
619
|
+
"p_agent_filter": agent_filter or None,
|
|
620
|
+
})
|
|
621
|
+
if isinstance(rpc_result, dict) and rpc_result.get("ok"):
|
|
622
|
+
return rpc_result.get("messages", [])
|
|
623
|
+
# Fall through to direct query if RPC doesn't exist yet
|
|
624
|
+
|
|
625
|
+
# Fallback: direct SELECT (tests/legacy)
|
|
599
626
|
filters = f"project_id=eq.{project_id}&type=neq.ack"
|
|
600
627
|
if agent_filter:
|
|
601
628
|
filters += f"&or=(from_agent.eq.{quote(agent_filter)},to_agent.eq.{quote(agent_filter)})"
|
|
@@ -607,7 +634,21 @@ def get_history(project_id: str, limit: int = 20, agent_filter: str = "") -> Lis
|
|
|
607
634
|
)
|
|
608
635
|
|
|
609
636
|
|
|
610
|
-
def get_message_by_id(project_id: str, msg_id: str) -> Optional[Dict]:
|
|
637
|
+
def get_message_by_id(project_id: str, msg_id: str, api_key: Optional[str] = None) -> Optional[Dict]:
|
|
638
|
+
# Use SECURITY DEFINER RPC when api_key is available (bypasses RLS safely)
|
|
639
|
+
if api_key:
|
|
640
|
+
rpc_result = sb_rpc("mc_get_message_by_id", {
|
|
641
|
+
"p_api_key": api_key,
|
|
642
|
+
"p_project_id": project_id,
|
|
643
|
+
"p_msg_id": msg_id,
|
|
644
|
+
})
|
|
645
|
+
if isinstance(rpc_result, dict) and rpc_result.get("ok"):
|
|
646
|
+
return rpc_result.get("message")
|
|
647
|
+
if isinstance(rpc_result, dict) and rpc_result.get("error"):
|
|
648
|
+
return None
|
|
649
|
+
# Fall through to direct query if RPC doesn't exist yet
|
|
650
|
+
|
|
651
|
+
# Fallback: direct SELECT (tests/legacy)
|
|
611
652
|
results = sb_select(
|
|
612
653
|
"mc_messages",
|
|
613
654
|
f"project_id=eq.{project_id}&id=eq.{msg_id}",
|
|
@@ -69,7 +69,11 @@ class RealtimeListener:
|
|
|
69
69
|
@property
|
|
70
70
|
def ws_url(self) -> str:
|
|
71
71
|
host = self.supabase_url.replace("https://", "").replace("http://", "").rstrip("/")
|
|
72
|
-
|
|
72
|
+
# Use service_role_key for WebSocket auth if available — bypasses RLS
|
|
73
|
+
# so Realtime can deliver INSERT events without needing auth.uid().
|
|
74
|
+
# Falls back to publishable key (requires anon RLS policy).
|
|
75
|
+
key = self.service_role_key or self.supabase_key
|
|
76
|
+
return f"wss://{host}/realtime/v1/websocket?apikey={key}&vsn=1.0.0"
|
|
73
77
|
|
|
74
78
|
async def start(self) -> None:
|
|
75
79
|
if not WEBSOCKETS_AVAILABLE:
|
|
@@ -129,21 +133,6 @@ class RealtimeListener:
|
|
|
129
133
|
self._connected = True
|
|
130
134
|
log.info(f"Realtime connected for agent={self.agent_name}")
|
|
131
135
|
|
|
132
|
-
# Elevate auth context if service_role_key is available.
|
|
133
|
-
# This lets Supabase Realtime bypass RLS for INSERT event delivery,
|
|
134
|
-
# so mc_user_has_project_access (which needs auth.uid()) is not required.
|
|
135
|
-
if self.service_role_key:
|
|
136
|
-
try:
|
|
137
|
-
await ws.send(json.dumps({
|
|
138
|
-
"topic": "realtime:*",
|
|
139
|
-
"event": "access_token",
|
|
140
|
-
"payload": {"access_token": self.service_role_key},
|
|
141
|
-
"ref": "auth",
|
|
142
|
-
}))
|
|
143
|
-
log.info("Realtime auth elevated with service_role_key")
|
|
144
|
-
except Exception as e:
|
|
145
|
-
log.warning(f"Failed to send access_token: {e}")
|
|
146
|
-
|
|
147
136
|
# Phoenix channel join: phoenix realtime topic
|
|
148
137
|
topic = f"realtime:{self.project_id}-{self.agent_name}"
|
|
149
138
|
join_msg = {
|
|
@@ -357,31 +357,15 @@ if isinstance(_register_result, dict) and _register_result.get("error"):
|
|
|
357
357
|
# bypass RLS — the publishable key has no JWT context and cannot UPDATE
|
|
358
358
|
# mc_agents directly. The RPC validates ownership via api_key.
|
|
359
359
|
def _flip_status(status: str, task: str = "") -> bool:
|
|
360
|
-
"""Write status
|
|
360
|
+
"""Write status via RPC for reliable updates (anon PATCH silently fails).
|
|
361
361
|
|
|
362
|
-
|
|
363
|
-
immediately — the dashboard sees the change in <100ms instead of waiting
|
|
364
|
-
for the next heartbeat cycle.
|
|
362
|
+
Falls back to direct PATCH only when no api_key is available (tests).
|
|
365
363
|
"""
|
|
366
364
|
try:
|
|
367
|
-
be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
|
|
368
|
-
return
|
|
365
|
+
result = be.set_status(_PROJECT_ID, AGENT_NAME, status, task, api_key=_get_api_key())
|
|
366
|
+
return isinstance(result, dict) and result.get("ok", False)
|
|
369
367
|
except Exception:
|
|
370
|
-
|
|
371
|
-
api_key = _get_api_key()
|
|
372
|
-
if not api_key:
|
|
373
|
-
return False
|
|
374
|
-
try:
|
|
375
|
-
r = be.sb_rpc("mc_agent_set_status_by_api_key", {
|
|
376
|
-
"p_api_key": api_key,
|
|
377
|
-
"p_project_id": _PROJECT_ID,
|
|
378
|
-
"p_agent_name": AGENT_NAME,
|
|
379
|
-
"p_status": status,
|
|
380
|
-
"p_task": task,
|
|
381
|
-
})
|
|
382
|
-
return isinstance(r, dict) and r.get("ok", False)
|
|
383
|
-
except Exception:
|
|
384
|
-
return False
|
|
368
|
+
return False
|
|
385
369
|
|
|
386
370
|
if not _flip_status("idle", ""):
|
|
387
371
|
print(f"[meshcode-mcp] WARNING: could not flip status to idle", file=sys.stderr)
|
|
@@ -578,7 +562,7 @@ def _acquire_lease() -> bool:
|
|
|
578
562
|
except Exception:
|
|
579
563
|
# Force clear via direct update
|
|
580
564
|
try:
|
|
581
|
-
be.set_status(_PROJECT_ID, AGENT_NAME, "offline", "")
|
|
565
|
+
be.set_status(_PROJECT_ID, AGENT_NAME, "offline", "", api_key=_get_api_key())
|
|
582
566
|
except Exception:
|
|
583
567
|
pass
|
|
584
568
|
_time.sleep(1)
|
|
@@ -1000,7 +984,7 @@ def _heartbeat_thread_fn():
|
|
|
1000
984
|
|
|
1001
985
|
# Sync current state to DB (in case realtime missed it)
|
|
1002
986
|
try:
|
|
1003
|
-
be.set_status(_PROJECT_ID, AGENT_NAME, _current_state, _current_tool)
|
|
987
|
+
be.set_status(_PROJECT_ID, AGENT_NAME, _current_state, _current_tool, api_key=_get_api_key())
|
|
1004
988
|
except Exception:
|
|
1005
989
|
pass
|
|
1006
990
|
if _REALTIME and not _REALTIME.is_connected:
|
|
@@ -1049,7 +1033,7 @@ async def lifespan(_app):
|
|
|
1049
1033
|
for _attempt in range(3):
|
|
1050
1034
|
try:
|
|
1051
1035
|
be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME, "p_version": "2.0.0"})
|
|
1052
|
-
be.set_status(_PROJECT_ID, AGENT_NAME, "idle", "MCP session active")
|
|
1036
|
+
be.set_status(_PROJECT_ID, AGENT_NAME, "idle", "MCP session active", api_key=_get_api_key())
|
|
1053
1037
|
log.info(f"[meshcode] Agent {AGENT_NAME} online — initial heartbeat sent")
|
|
1054
1038
|
break
|
|
1055
1039
|
except Exception as e:
|
|
@@ -1128,9 +1112,9 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
|
|
|
1128
1112
|
sensitive: bool = False, encrypted: bool = False) -> Dict[str, Any]:
|
|
1129
1113
|
"""Send message. Use "agent@meshwork" for cross-mesh. sensitive=True hides from exports. Pass encrypted=True for secrets/credentials (AES-256-GCM)."""
|
|
1130
1114
|
if isinstance(message, str):
|
|
1131
|
-
# Auto-wrap strings
|
|
1132
|
-
if len(message) >
|
|
1133
|
-
return {"error": f"
|
|
1115
|
+
# Auto-wrap strings into dict. Warn if very long but don't reject.
|
|
1116
|
+
if len(message) > 2000:
|
|
1117
|
+
return {"error": f"message too long ({len(message)} chars). Use meshcode_task_create for long content, or split into multiple messages. Max ~2000 chars."}
|
|
1134
1118
|
payload: Dict[str, Any] = {"text": message}
|
|
1135
1119
|
elif isinstance(message, dict):
|
|
1136
1120
|
payload = message
|
|
@@ -1237,7 +1221,7 @@ def meshcode_read(include_acks: bool = False) -> Dict[str, Any]:
|
|
|
1237
1221
|
@with_working_status
|
|
1238
1222
|
def meshcode_history(limit: int = 20, agent_filter: Optional[str] = None) -> Dict[str, Any]:
|
|
1239
1223
|
"""View past messages (read+unread). Optional agent_filter."""
|
|
1240
|
-
raw = be.get_history(_PROJECT_ID, limit=limit, agent_filter=agent_filter or "")
|
|
1224
|
+
raw = be.get_history(_PROJECT_ID, limit=limit, agent_filter=agent_filter or "", api_key=_get_api_key())
|
|
1241
1225
|
messages = [
|
|
1242
1226
|
{
|
|
1243
1227
|
"from": m.get("from_agent", ""),
|
|
@@ -1257,7 +1241,7 @@ def meshcode_history(limit: int = 20, agent_filter: Optional[str] = None) -> Dic
|
|
|
1257
1241
|
@with_working_status
|
|
1258
1242
|
def meshcode_read_message(msg_id: str) -> Dict[str, Any]:
|
|
1259
1243
|
"""Fetch a specific message by its UUID."""
|
|
1260
|
-
msg = be.get_message_by_id(_PROJECT_ID, msg_id)
|
|
1244
|
+
msg = be.get_message_by_id(_PROJECT_ID, msg_id, api_key=_get_api_key())
|
|
1261
1245
|
if not msg:
|
|
1262
1246
|
return {"error": "message not found", "msg_id": msg_id}
|
|
1263
1247
|
return {
|
|
@@ -1313,7 +1297,12 @@ except Exception as _e:
|
|
|
1313
1297
|
|
|
1314
1298
|
|
|
1315
1299
|
def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
|
|
1316
|
-
"""Fetch
|
|
1300
|
+
"""Fetch tasks that THIS agent should work on. Returns compact list or None.
|
|
1301
|
+
|
|
1302
|
+
Only blocks on tasks directly assigned to this agent or claimed by this agent.
|
|
1303
|
+
Tasks assigned to '*' (wildcard) that are unclaimed are NOT blocking —
|
|
1304
|
+
they're available for any agent but shouldn't prevent wait().
|
|
1305
|
+
"""
|
|
1317
1306
|
try:
|
|
1318
1307
|
api_key = _get_api_key()
|
|
1319
1308
|
if not api_key:
|
|
@@ -1326,7 +1315,10 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
|
|
|
1326
1315
|
{"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
|
|
1327
1316
|
for t in tasks
|
|
1328
1317
|
if t.get("status") in ("open", "in_progress")
|
|
1329
|
-
and (
|
|
1318
|
+
and (
|
|
1319
|
+
t.get("claimed_by") == AGENT_NAME # I claimed it — must work it
|
|
1320
|
+
or t.get("assignee") == AGENT_NAME # Directly assigned to me
|
|
1321
|
+
)
|
|
1330
1322
|
]
|
|
1331
1323
|
return pending if pending else None
|
|
1332
1324
|
except Exception:
|
|
@@ -1370,13 +1362,19 @@ async def meshcode_wait(timeout_seconds: int = 120, include_acks: bool = False)
|
|
|
1370
1362
|
"id": m.get("id"), "parent_id": m.get("parent_msg_id")}
|
|
1371
1363
|
for m in raw
|
|
1372
1364
|
]
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1365
|
+
# Dedup against already-seen messages (fixes race where
|
|
1366
|
+
# background mark_read hasn't completed yet)
|
|
1367
|
+
deduped = _filter_and_mark(msgs)
|
|
1368
|
+
if not deduped:
|
|
1369
|
+
pass # All messages already seen — fall through to wait loop
|
|
1370
|
+
else:
|
|
1371
|
+
split = _split_messages(deduped)
|
|
1372
|
+
return {
|
|
1373
|
+
"refused": True,
|
|
1374
|
+
"reason": f"You have {len(deduped)} unread messages. Process them before waiting.",
|
|
1375
|
+
"got_message": True,
|
|
1376
|
+
**split,
|
|
1377
|
+
}
|
|
1380
1378
|
except Exception:
|
|
1381
1379
|
pass
|
|
1382
1380
|
|
|
@@ -1732,7 +1730,7 @@ def meshcode_set_status(status: str, task: str = "") -> Dict[str, Any]:
|
|
|
1732
1730
|
"reason": f"Cannot set status '{status}' — you have {len(pending_tasks)} open tasks. Work them first.",
|
|
1733
1731
|
"pending_tasks": pending_tasks,
|
|
1734
1732
|
}
|
|
1735
|
-
return be.set_status(_PROJECT_ID, AGENT_NAME, status, task)
|
|
1733
|
+
return be.set_status(_PROJECT_ID, AGENT_NAME, status, task, api_key=_get_api_key())
|
|
1736
1734
|
|
|
1737
1735
|
|
|
1738
1736
|
@mcp.tool()
|
|
@@ -2064,7 +2062,7 @@ def meshcode_scratchpad_set(key: str, value: Any) -> Dict[str, Any]:
|
|
|
2064
2062
|
"p_api_key": api_key,
|
|
2065
2063
|
"p_key": key,
|
|
2066
2064
|
"p_value": json_value,
|
|
2067
|
-
"
|
|
2065
|
+
"p_tier": "reference",
|
|
2068
2066
|
})
|
|
2069
2067
|
|
|
2070
2068
|
|
|
@@ -2078,9 +2076,9 @@ def meshcode_scratchpad_get(key: Optional[str] = None) -> Dict[str, Any]:
|
|
|
2078
2076
|
"""
|
|
2079
2077
|
api_key = _get_api_key()
|
|
2080
2078
|
if key:
|
|
2081
|
-
return be.sb_rpc("mc_scratchpad_get", {"p_api_key": api_key, "p_key": key
|
|
2079
|
+
return be.sb_rpc("mc_scratchpad_get", {"p_api_key": api_key, "p_key": key})
|
|
2082
2080
|
else:
|
|
2083
|
-
return be.sb_rpc("mc_scratchpad_list", {"p_api_key": api_key
|
|
2081
|
+
return be.sb_rpc("mc_scratchpad_list", {"p_api_key": api_key})
|
|
2084
2082
|
|
|
2085
2083
|
|
|
2086
2084
|
# ----------------- OBSIDIAN SYNC HELPER -----------------
|
|
@@ -2297,7 +2295,7 @@ def board_resource() -> str:
|
|
|
2297
2295
|
@mcp.resource("meshcode://history")
|
|
2298
2296
|
def history_resource() -> str:
|
|
2299
2297
|
"""Recent message history for this meshwork (last 20 non-ack messages)."""
|
|
2300
|
-
history = be.get_history(_PROJECT_ID, limit=20)
|
|
2298
|
+
history = be.get_history(_PROJECT_ID, limit=20, api_key=_get_api_key())
|
|
2301
2299
|
return json.dumps({
|
|
2302
2300
|
"project": PROJECT_NAME,
|
|
2303
2301
|
"history": history,
|
|
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
|
|
File without changes
|