meshcode 2.11.127__tar.gz → 2.11.129__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.11.127 → meshcode-2.11.129}/PKG-INFO +2 -11
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/__init__.py +1 -1
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/backend.py +11 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/server.py +43 -6
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/secrets.py +51 -11
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode.egg-info/PKG-INFO +2 -11
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode.egg-info/SOURCES.txt +2 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/pyproject.toml +9 -1
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_boot_bug_regression.py +1 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_cross_agent_messaging.py +34 -10
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_epistemic_v1_stop_conditions.py +1 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_esc_deaf_state.py +7 -1
- meshcode-2.11.129/tests/test_live_mesh_guard.py +100 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_mark_read_batch.py +34 -11
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_rls_cross_tenant.py +19 -5
- meshcode-2.11.129/tests/test_session_replay_gate.py +153 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/README.md +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/__main__.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/cli.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/compat.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/daemon.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/doctor.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/hostd.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/invites.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/launcher.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/swarm.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_swarm.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/preferences.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/run_agent.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/self_update.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/up.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode/upload.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/setup.cfg +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_core.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_doctor.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_stop_ghost_terminal.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_swarm_events.py +0 -0
- {meshcode-2.11.127 → meshcode-2.11.129}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: meshcode
|
|
3
|
-
Version: 2.11.
|
|
3
|
+
Version: 2.11.129
|
|
4
4
|
Summary: Real-time communication between AI agents — Supabase-backed CLI
|
|
5
5
|
Author-email: MeshCode <hello@meshcode.io>
|
|
6
6
|
License: MIT
|
|
@@ -18,17 +18,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Operating System :: OS Independent
|
|
19
19
|
Requires-Python: >=3.9
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: mcp[cli]>=1.0.0
|
|
22
|
-
Requires-Dist: websockets>=12.0
|
|
23
|
-
Requires-Dist: realtime>=2.0.0
|
|
24
|
-
Requires-Dist: keyring>=24.0
|
|
25
|
-
Requires-Dist: cryptography>=41.0
|
|
26
21
|
Provides-Extra: test
|
|
27
|
-
Requires-Dist: pytest>=8; extra == "test"
|
|
28
22
|
Provides-Extra: dev
|
|
29
|
-
Requires-Dist: build>=1.0; extra == "dev"
|
|
30
|
-
Requires-Dist: twine>=4; extra == "dev"
|
|
31
|
-
Requires-Dist: pytest>=8; extra == "dev"
|
|
32
23
|
|
|
33
24
|
# MeshCode
|
|
34
25
|
|
|
@@ -1327,6 +1327,17 @@ def task_complete(api_key, project_id, task_id, completing_agent, summary=""):
|
|
|
1327
1327
|
})
|
|
1328
1328
|
|
|
1329
1329
|
|
|
1330
|
+
def feature_flag_enabled(api_key, flag_name):
|
|
1331
|
+
"""Agent-facing feature-flag check (mig478). The FE's mc_check_feature_flag
|
|
1332
|
+
is auth.uid()-only, so agents use this api-key variant. Returns the raw
|
|
1333
|
+
RPC dict ({"ok": true, "enabled": bool}); callers treat anything else as
|
|
1334
|
+
disabled."""
|
|
1335
|
+
return sb_rpc("mc_feature_flag_enabled", {
|
|
1336
|
+
"p_api_key": api_key,
|
|
1337
|
+
"p_flag_name": flag_name,
|
|
1338
|
+
})
|
|
1339
|
+
|
|
1340
|
+
|
|
1330
1341
|
# Per-(project,agent,session,event_type) min interval. Same idea as task_list:
|
|
1331
1342
|
# fire-and-forget pollers that hammer record_event at >2/sec are throttled
|
|
1332
1343
|
# client-side. Identical events within the window are dropped (returns
|
|
@@ -609,16 +609,53 @@ import uuid as _uuid
|
|
|
609
609
|
_SESSION_ID = str(_uuid.uuid4())[:12]
|
|
610
610
|
|
|
611
611
|
|
|
612
|
+
# DB F2.1 (task 7f016375): tool_call events are 82% of mc_session_events
|
|
613
|
+
# storage and their only dashboard consumer (session replay / timeline) is
|
|
614
|
+
# gated behind the ff_session_replay feature flag — so the WRITE is gated by
|
|
615
|
+
# the same flag. Feature-flag gate, NOT sampling (a partial replay lies when
|
|
616
|
+
# debugging). ONLY 'tool_call' is gated; lifecycle events (boot/shutdown/
|
|
617
|
+
# error/status_change) always record. ORTHOGONAL to the 2.11.33 M3a throttle
|
|
618
|
+
# in backend.record_event: that one limits the RATE of whatever is enabled,
|
|
619
|
+
# this one decides IF tool_call records at all. The flag is also enforced
|
|
620
|
+
# server-side inside mc_record_event (mig488) — that covers old clients; this
|
|
621
|
+
# client cache just saves the network call. Fail-closed: unknown flag state
|
|
622
|
+
# records nothing (storage emergency beats replay completeness).
|
|
623
|
+
_FF_REPLAY_TTL_SEC = 600.0 # re-check the flag every 10 min per process
|
|
624
|
+
_ff_replay_cache = {"enabled": None, "checked_mono": 0.0}
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _session_replay_enabled() -> bool:
|
|
628
|
+
"""Cached ff_session_replay lookup. Runs INSIDE the bg record thread, so a
|
|
629
|
+
slow/failed flag fetch never adds latency to a tool call."""
|
|
630
|
+
now = _time.monotonic()
|
|
631
|
+
cache = _ff_replay_cache
|
|
632
|
+
if cache["enabled"] is None or now - cache["checked_mono"] >= _FF_REPLAY_TTL_SEC:
|
|
633
|
+
try:
|
|
634
|
+
res = be.feature_flag_enabled(_get_api_key(), "ff_session_replay")
|
|
635
|
+
cache["enabled"] = bool(isinstance(res, dict) and res.get("enabled"))
|
|
636
|
+
except Exception:
|
|
637
|
+
# Keep the last known answer; with no answer yet, fail closed.
|
|
638
|
+
if cache["enabled"] is None:
|
|
639
|
+
cache["enabled"] = False
|
|
640
|
+
cache["checked_mono"] = now
|
|
641
|
+
return cache["enabled"]
|
|
642
|
+
|
|
643
|
+
|
|
612
644
|
def _record_event_bg(event_type: str, payload: dict = None) -> None:
|
|
613
|
-
"""Fire-and-forget: record a session event in background thread.
|
|
645
|
+
"""Fire-and-forget: record a session event in background thread.
|
|
646
|
+
'tool_call' events are dropped while ff_session_replay is off (task
|
|
647
|
+
7f016375); every other event type records unconditionally."""
|
|
614
648
|
try:
|
|
615
649
|
api_key = _get_api_key()
|
|
616
650
|
import threading
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
651
|
+
|
|
652
|
+
def _record():
|
|
653
|
+
if event_type == "tool_call" and not _session_replay_enabled():
|
|
654
|
+
return
|
|
655
|
+
be.record_event(api_key, _PROJECT_ID, AGENT_NAME, _SESSION_ID,
|
|
656
|
+
event_type, payload or {})
|
|
657
|
+
|
|
658
|
+
threading.Thread(target=_record, daemon=True).start()
|
|
622
659
|
except Exception:
|
|
623
660
|
pass
|
|
624
661
|
|
|
@@ -97,12 +97,45 @@ def _probe_keyring() -> bool:
|
|
|
97
97
|
_KEYRING_BACKEND_NAME = type(backend).__name__
|
|
98
98
|
# Some installs report a "fail" backend on headless Linux. Probe with a
|
|
99
99
|
# write+read+delete round trip on a sentinel key.
|
|
100
|
-
|
|
100
|
+
# BUG 6b28a32f: the sentinel MUST be unique per process. With a fixed
|
|
101
|
+
# name, N concurrent CLIs (hostd respawn sweeps spawn up to 3 in the
|
|
102
|
+
# same second) race set/get/delete on the SAME wincred entry — the
|
|
103
|
+
# loser reads None (or its delete raises because a sibling already
|
|
104
|
+
# deleted it) and wrongly concludes the keychain is unusable, which
|
|
105
|
+
# cascades into "[meshcode] Not logged in" + a dead terminal tab.
|
|
106
|
+
# Fast path (race-immune): if we can READ an existing entry, the
|
|
107
|
+
# backend is proven usable — no write probe needed. The write probe
|
|
108
|
+
# below is only for fresh installs with no entry yet (interactive
|
|
109
|
+
# `meshcode login`, single process, no concurrency).
|
|
110
|
+
try:
|
|
111
|
+
if keyring.get_password(SERVICE_NAME, DEFAULT_PROFILE):
|
|
112
|
+
_KEYRING_AVAILABLE = True
|
|
113
|
+
return True
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
sentinel_key = f"meshcode_probe_{os.getpid()}_{_stdlib_secrets.token_hex(4)}"
|
|
101
117
|
sentinel_val = "ok"
|
|
102
118
|
try:
|
|
119
|
+
import time as _time
|
|
103
120
|
keyring.set_password(SERVICE_NAME, sentinel_key, sentinel_val)
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
# Windows CredMan has read-after-write visibility lag under
|
|
122
|
+
# concurrent vault access (verified 6b28a32f: CredWrite succeeds,
|
|
123
|
+
# immediate CredRead misses, the entry IS there later). Retry the
|
|
124
|
+
# read-back briefly before declaring the backend unusable.
|
|
125
|
+
got = None
|
|
126
|
+
for _attempt in range(4):
|
|
127
|
+
got = keyring.get_password(SERVICE_NAME, sentinel_key)
|
|
128
|
+
if got == sentinel_val:
|
|
129
|
+
break
|
|
130
|
+
_time.sleep(0.05 * (_attempt + 1))
|
|
131
|
+
for _attempt in range(2):
|
|
132
|
+
try:
|
|
133
|
+
keyring.delete_password(SERVICE_NAME, sentinel_key)
|
|
134
|
+
break
|
|
135
|
+
except Exception:
|
|
136
|
+
# cleanup failure does not mean the backend is unusable;
|
|
137
|
+
# one delayed retry keeps orphan sentinels out of the vault
|
|
138
|
+
_time.sleep(0.05)
|
|
106
139
|
_KEYRING_AVAILABLE = (got == sentinel_val)
|
|
107
140
|
except KeyringError:
|
|
108
141
|
_KEYRING_AVAILABLE = False
|
|
@@ -218,15 +251,22 @@ def get_api_key(profile: str = DEFAULT_PROFILE) -> Optional[str]:
|
|
|
218
251
|
1. Keychain entry (SERVICE_NAME, profile)
|
|
219
252
|
2. ~/.meshcode/credentials.<profile>.json (fallback file from set_api_key)
|
|
220
253
|
3. ~/.meshcode/credentials.json (legacy pre-1.4.1 file, default profile only)
|
|
254
|
+
|
|
255
|
+
BUG 6b28a32f: READS are no longer gated on _probe_keyring(). The probe
|
|
256
|
+
does a write+read+delete round trip, which is only meaningful before a
|
|
257
|
+
WRITE (set_api_key) — gating reads on it meant any transient probe
|
|
258
|
+
failure (concurrent-CLI sentinel race, vault hiccup) skipped a perfectly
|
|
259
|
+
healthy keychain entry and fell through to fallback files that were
|
|
260
|
+
migrated-and-shredded, producing a false "Not logged in". Just try the
|
|
261
|
+
read; it is its own probe.
|
|
221
262
|
"""
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
pass
|
|
263
|
+
try:
|
|
264
|
+
import keyring # type: ignore
|
|
265
|
+
val = keyring.get_password(SERVICE_NAME, profile)
|
|
266
|
+
if val:
|
|
267
|
+
return val
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
230
270
|
|
|
231
271
|
# Fallback file for this profile
|
|
232
272
|
fallback_path = Path.home() / ".meshcode" / f"credentials.{profile}.json"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: meshcode
|
|
3
|
-
Version: 2.11.
|
|
3
|
+
Version: 2.11.129
|
|
4
4
|
Summary: Real-time communication between AI agents — Supabase-backed CLI
|
|
5
5
|
Author-email: MeshCode <hello@meshcode.io>
|
|
6
6
|
License: MIT
|
|
@@ -18,17 +18,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Operating System :: OS Independent
|
|
19
19
|
Requires-Python: >=3.9
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: mcp[cli]>=1.0.0
|
|
22
|
-
Requires-Dist: websockets>=12.0
|
|
23
|
-
Requires-Dist: realtime>=2.0.0
|
|
24
|
-
Requires-Dist: keyring>=24.0
|
|
25
|
-
Requires-Dist: cryptography>=41.0
|
|
26
21
|
Provides-Extra: test
|
|
27
|
-
Requires-Dist: pytest>=8; extra == "test"
|
|
28
22
|
Provides-Extra: dev
|
|
29
|
-
Requires-Dist: build>=1.0; extra == "dev"
|
|
30
|
-
Requires-Dist: twine>=4; extra == "dev"
|
|
31
|
-
Requires-Dist: pytest>=8; extra == "dev"
|
|
32
23
|
|
|
33
24
|
# MeshCode
|
|
34
25
|
|
|
@@ -74,6 +74,7 @@ tests/test_file_upload.py
|
|
|
74
74
|
tests/test_init_device_code.py
|
|
75
75
|
tests/test_install_guard.py
|
|
76
76
|
tests/test_lease_sigterm_release.py
|
|
77
|
+
tests/test_live_mesh_guard.py
|
|
77
78
|
tests/test_mark_read_batch.py
|
|
78
79
|
tests/test_marketplace_ratings.py
|
|
79
80
|
tests/test_migration_integrity.py
|
|
@@ -86,6 +87,7 @@ tests/test_run_agent_no_server_import.py
|
|
|
86
87
|
tests/test_security_regressions.py
|
|
87
88
|
tests/test_self_update_user_site.py
|
|
88
89
|
tests/test_sentinel.py
|
|
90
|
+
tests/test_session_replay_gate.py
|
|
89
91
|
tests/test_setup_path.py
|
|
90
92
|
tests/test_sleep_signals.py
|
|
91
93
|
tests/test_status_enum_coverage.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "meshcode"
|
|
7
|
-
version = "2.11.
|
|
7
|
+
version = "2.11.129"
|
|
8
8
|
description = "Real-time communication between AI agents — Supabase-backed CLI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -45,6 +45,14 @@ Repository = "https://github.com/rf2f7f7sg4-dev/meshcode"
|
|
|
45
45
|
[tool.pytest.ini_options]
|
|
46
46
|
testpaths = ["tests"]
|
|
47
47
|
addopts = "--durations=10 --durations-min=0.5 -v --tb=short"
|
|
48
|
+
markers = [
|
|
49
|
+
# live_mesh: the test talks to the REAL Supabase backend (registers agents,
|
|
50
|
+
# sends messages, reads the live roster). Opt-in via MESHCODE_TEST_PROJECT;
|
|
51
|
+
# CI/smoke must deselect with: -m "not live_mesh"
|
|
52
|
+
# (incident 2026-06-10: suite runs leaked 30 fixture agents into the live
|
|
53
|
+
# mesh-dev roster on Ian's dashboard).
|
|
54
|
+
"live_mesh: hits the real Supabase backend/live mesh; opt-in, deselect with -m 'not live_mesh'",
|
|
55
|
+
]
|
|
48
56
|
|
|
49
57
|
[tool.setuptools.packages.find]
|
|
50
58
|
include = ["meshcode", "meshcode.*"]
|
|
@@ -21,6 +21,14 @@ import time
|
|
|
21
21
|
import unittest
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
# These tests register agents and send messages on the REAL backend — every
|
|
27
|
+
# run is visible on the owner's dashboard. Opt-in only (incident 2026-06-10:
|
|
28
|
+
# fixture agents leaked into the live mesh-dev roster). Deselect in CI/smoke
|
|
29
|
+
# with: -m "not live_mesh"
|
|
30
|
+
pytestmark = pytest.mark.live_mesh
|
|
31
|
+
|
|
24
32
|
# Add SDK to path
|
|
25
33
|
SDK_PATH = Path(__file__).parent.parent / "meshcode" / "meshcode_mcp"
|
|
26
34
|
if SDK_PATH.exists():
|
|
@@ -39,7 +47,10 @@ from meshcode.meshcode_mcp import backend as be
|
|
|
39
47
|
# Agents are cleaned up in tearDown. No project creation needed.
|
|
40
48
|
#
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
# NEVER default to a live mesh. Unset MESHCODE_TEST_PROJECT = SKIP everything:
|
|
51
|
+
# the old default ("mesh-dev") put every fixture agent and broadcast of this
|
|
52
|
+
# suite straight onto the production dashboard (incident 2026-06-10).
|
|
53
|
+
TEST_PROJECT = os.environ.get("MESHCODE_TEST_PROJECT")
|
|
43
54
|
AGENT_A = f"e2e-alice-{int(time.time()) % 10000}"
|
|
44
55
|
AGENT_B = f"e2e-bob-{int(time.time()) % 10000}"
|
|
45
56
|
AGENT_C = f"e2e-carol-{int(time.time()) % 10000}"
|
|
@@ -61,6 +72,10 @@ def _get_api_key():
|
|
|
61
72
|
API_KEY = _get_api_key()
|
|
62
73
|
|
|
63
74
|
|
|
75
|
+
@unittest.skipUnless(
|
|
76
|
+
TEST_PROJECT,
|
|
77
|
+
"MESHCODE_TEST_PROJECT not set — live-mesh e2e tests are opt-in and must "
|
|
78
|
+
"point at a dedicated test meshwork, never a live one (incident 2026-06-10)")
|
|
64
79
|
class CrossAgentMessagingTests(unittest.TestCase):
|
|
65
80
|
"""E2E tests for cross-agent message delivery."""
|
|
66
81
|
|
|
@@ -106,16 +121,25 @@ class CrossAgentMessagingTests(unittest.TestCase):
|
|
|
106
121
|
|
|
107
122
|
@classmethod
|
|
108
123
|
def tearDownClass(cls):
|
|
109
|
-
"""Cleanup: unregister ephemeral test agents.
|
|
124
|
+
"""Cleanup: unregister ephemeral test agents. LOUD on failure — the old
|
|
125
|
+
except:pass (plus never checking sb_rpc's returned error dict) let a
|
|
126
|
+
404 on mc_unregister_agent leak fixture agents into the roster forever
|
|
127
|
+
(incident 2026-06-10)."""
|
|
128
|
+
failures = []
|
|
110
129
|
for agent in [AGENT_A, AGENT_B, AGENT_C]:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
result = be.sb_rpc("mc_unregister_agent", {
|
|
131
|
+
"p_api_key": API_KEY,
|
|
132
|
+
"p_project_id": cls.project_id,
|
|
133
|
+
"p_agent_name": agent,
|
|
134
|
+
})
|
|
135
|
+
# sb_rpc reports errors as a return VALUE, not an exception.
|
|
136
|
+
if isinstance(result, dict) and (result.get("error") or result.get("_error")):
|
|
137
|
+
failures.append(f"{agent}: {result}")
|
|
138
|
+
if failures:
|
|
139
|
+
raise AssertionError(
|
|
140
|
+
"tearDown could not unregister test agents — they are now "
|
|
141
|
+
f"ORPHANED in '{TEST_PROJECT}' and must be removed by hand:\n "
|
|
142
|
+
+ "\n ".join(failures))
|
|
119
143
|
print(f"\n Cleaned up test agents from {TEST_PROJECT}")
|
|
120
144
|
|
|
121
145
|
# ── Basic Delivery ─────────────────────────────────────────────────────
|
|
@@ -33,6 +33,7 @@ def _live_or_skip():
|
|
|
33
33
|
pytest.skip("MESHCODE_SUPABASE_URL not set; epistemic V1 health is live-only.")
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
@pytest.mark.live_mesh
|
|
36
37
|
def test_epistemic_v1_health_all_conditions_healthy():
|
|
37
38
|
"""Live check: call mc_epistemic_v1_health() and assert each is_healthy."""
|
|
38
39
|
_live_or_skip()
|
|
@@ -20,6 +20,8 @@ import os
|
|
|
20
20
|
import sys
|
|
21
21
|
import time
|
|
22
22
|
import unittest
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
23
25
|
from pathlib import Path
|
|
24
26
|
from unittest.mock import patch, MagicMock, AsyncMock
|
|
25
27
|
|
|
@@ -196,8 +198,12 @@ class TestCircuitBreakerAfterCancel(unittest.TestCase):
|
|
|
196
198
|
# Not a hard failure — pool may recover via other means
|
|
197
199
|
|
|
198
200
|
|
|
201
|
+
@pytest.mark.live_mesh
|
|
199
202
|
class TestBackendOperationsAfterCancel(unittest.TestCase):
|
|
200
|
-
"""Test that backend send/read work after simulated interrupt state.
|
|
203
|
+
"""Test that backend send/read work after simulated interrupt state.
|
|
204
|
+
|
|
205
|
+
live_mesh: test_06 reads count_pending against the REAL backend with the
|
|
206
|
+
keyring credential (read-only, but still a live-DB dependency)."""
|
|
201
207
|
|
|
202
208
|
def test_06_send_message_after_cb_reset(self):
|
|
203
209
|
"""send_message should work even if circuit breaker was previously open."""
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Live-Mesh Guard Regression Tests
|
|
3
|
+
=================================
|
|
4
|
+
Enforces the suite-wide rules from incident 2026-06-10 (30 fixture agents +
|
|
5
|
+
broadcast/impersonation spam leaked into the LIVE mesh-dev roster on the
|
|
6
|
+
owner's dashboard because e2e tests defaulted TEST_PROJECT to "mesh-dev" and
|
|
7
|
+
their teardown swallowed a 404 on a nonexistent RPC):
|
|
8
|
+
|
|
9
|
+
1. No test may DEFAULT its target project to a live mesh — the project must
|
|
10
|
+
come from MESHCODE_TEST_PROJECT with no fallback value.
|
|
11
|
+
2. Any test file that registers agents on the real backend must carry the
|
|
12
|
+
`live_mesh` pytest marker so CI/smoke can deselect with -m "not live_mesh".
|
|
13
|
+
3. mc_unregister_agent teardowns must CHECK the sb_rpc result (errors come
|
|
14
|
+
back as a return value, not an exception) — no silent leaks.
|
|
15
|
+
4. The live_mesh marker stays registered in pyproject.toml.
|
|
16
|
+
|
|
17
|
+
Source-parsing only (pattern of test_security_regressions.py) — no DB needed.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
pytest tests/test_live_mesh_guard.py -v
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
TESTS_DIR = Path(__file__).parent
|
|
27
|
+
PYPROJECT = TESTS_DIR.parent / "pyproject.toml"
|
|
28
|
+
THIS_FILE = Path(__file__).name
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _test_sources():
|
|
32
|
+
for f in sorted(TESTS_DIR.glob("test_*.py")):
|
|
33
|
+
if f.name == THIS_FILE:
|
|
34
|
+
continue
|
|
35
|
+
yield f.name, f.read_text(errors="replace")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _code_lines(src: str):
|
|
39
|
+
"""Lines with #-comments stripped (docstring prose is still included, so
|
|
40
|
+
the patterns below target call/assignment syntax, not bare words)."""
|
|
41
|
+
return [ln.split("#", 1)[0] for ln in src.splitlines()]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestNoLiveMeshDefaults:
|
|
45
|
+
def test_no_default_value_for_test_project_env(self):
|
|
46
|
+
"""os.environ.get("MESHCODE_TEST_PROJECT", <default>) is forbidden —
|
|
47
|
+
unset must mean SKIP, never a fallback mesh."""
|
|
48
|
+
rx = re.compile(r"""MESHCODE_TEST_PROJECT['"]\s*,""")
|
|
49
|
+
offenders = [name for name, src in _test_sources()
|
|
50
|
+
for ln in _code_lines(src) if rx.search(ln)]
|
|
51
|
+
assert not offenders, (
|
|
52
|
+
f"MESHCODE_TEST_PROJECT must have NO default value (incident "
|
|
53
|
+
f"2026-06-10) — offenders: {offenders}")
|
|
54
|
+
|
|
55
|
+
def test_no_hardcoded_live_project_assignment(self):
|
|
56
|
+
"""A module-level <X>PROJECT = "mesh-dev" (or any literal) hardcodes a
|
|
57
|
+
live mesh as the test target."""
|
|
58
|
+
rx = re.compile(r"""^\s*\w*PROJECT\s*=\s*['"][a-zA-Z]""")
|
|
59
|
+
offenders = [name for name, src in _test_sources()
|
|
60
|
+
for ln in _code_lines(src) if rx.search(ln)]
|
|
61
|
+
assert not offenders, (
|
|
62
|
+
f"Test project names must come from MESHCODE_TEST_PROJECT, never "
|
|
63
|
+
f"a literal — offenders: {offenders}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestLiveMeshMarker:
|
|
67
|
+
def test_agent_registering_files_carry_marker(self):
|
|
68
|
+
"""Every file that calls mc_register_agent against the real backend
|
|
69
|
+
(i.e. without mocking sb_rpc) must be deselectable via the marker."""
|
|
70
|
+
for name, src in _test_sources():
|
|
71
|
+
code = "\n".join(_code_lines(src))
|
|
72
|
+
registers = re.search(r"""sb_rpc\(\s*['"]mc_register_agent['"]""", code)
|
|
73
|
+
mocked = re.search(r"patch.*sb_rpc|sb_rpc\s*=|monkeypatch", code)
|
|
74
|
+
if registers and not mocked:
|
|
75
|
+
assert re.search(r"pytestmark\s*=\s*pytest\.mark\.live_mesh", code), (
|
|
76
|
+
f"{name} registers agents on the live backend but has no "
|
|
77
|
+
f"module-level `pytestmark = pytest.mark.live_mesh`")
|
|
78
|
+
|
|
79
|
+
def test_marker_registered_in_pyproject(self):
|
|
80
|
+
cfg = PYPROJECT.read_text(errors="replace")
|
|
81
|
+
assert re.search(r"live_mesh\s*:", cfg), (
|
|
82
|
+
"live_mesh marker must stay registered under "
|
|
83
|
+
"[tool.pytest.ini_options] markers in pyproject.toml")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TestLoudTeardown:
|
|
87
|
+
def test_unregister_results_are_checked(self):
|
|
88
|
+
"""sb_rpc returns errors as a dict, not an exception — every
|
|
89
|
+
mc_unregister_agent caller must inspect the result."""
|
|
90
|
+
for name, src in _test_sources():
|
|
91
|
+
code = "\n".join(_code_lines(src))
|
|
92
|
+
if "mc_unregister_agent" not in code:
|
|
93
|
+
continue
|
|
94
|
+
# The call's result must be bound and error-checked.
|
|
95
|
+
assert re.search(r"=\s*be\.sb_rpc\(\s*['\"]mc_unregister_agent", code), (
|
|
96
|
+
f"{name}: mc_unregister_agent result is discarded — a 404 "
|
|
97
|
+
f"would silently leak fixture agents (incident 2026-06-10)")
|
|
98
|
+
assert re.search(r"""\.get\(\s*['"](_)?error['"]""", code), (
|
|
99
|
+
f"{name}: mc_unregister_agent result is never checked for an "
|
|
100
|
+
f"error key")
|
|
@@ -18,10 +18,17 @@ import time
|
|
|
18
18
|
import unittest
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
21
23
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
22
24
|
|
|
23
25
|
os.environ.setdefault("SUPABASE_URL", "https://gjinagyyjttyxnaoavnz.supabase.co")
|
|
24
26
|
|
|
27
|
+
# Registers agents and sends messages on the REAL backend — opt-in only
|
|
28
|
+
# (incident 2026-06-10: mread-* fixtures leaked into the live mesh-dev
|
|
29
|
+
# roster). Deselect in CI/smoke with: -m "not live_mesh"
|
|
30
|
+
pytestmark = pytest.mark.live_mesh
|
|
31
|
+
|
|
25
32
|
|
|
26
33
|
def _get_api_key():
|
|
27
34
|
key = os.environ.get("MESHCODE_API_KEY")
|
|
@@ -36,9 +43,16 @@ def _get_api_key():
|
|
|
36
43
|
|
|
37
44
|
|
|
38
45
|
API_KEY = _get_api_key()
|
|
39
|
-
|
|
46
|
+
# NEVER default to a live mesh (the old hardcoded "mesh-dev" put mread-*
|
|
47
|
+
# fixtures on the production dashboard). Unset = tests SKIP.
|
|
48
|
+
PROJECT = os.environ.get("MESHCODE_TEST_PROJECT")
|
|
40
49
|
PROJECT_ID = None
|
|
41
50
|
|
|
51
|
+
_LIVE_GATE = unittest.skipUnless(
|
|
52
|
+
API_KEY and PROJECT,
|
|
53
|
+
"MESHCODE_TEST_PROJECT (and an API key) required — live-mesh tests are "
|
|
54
|
+
"opt-in and must point at a dedicated test meshwork (incident 2026-06-10)")
|
|
55
|
+
|
|
42
56
|
|
|
43
57
|
def _resolve():
|
|
44
58
|
global PROJECT_ID
|
|
@@ -57,7 +71,7 @@ AGENT_A = f"mread-a-{int(time.time()) % 10000}"
|
|
|
57
71
|
AGENT_B = f"mread-b-{int(time.time()) % 10000}"
|
|
58
72
|
|
|
59
73
|
|
|
60
|
-
@
|
|
74
|
+
@_LIVE_GATE
|
|
61
75
|
class TestBatchMarkRead(unittest.TestCase):
|
|
62
76
|
|
|
63
77
|
@classmethod
|
|
@@ -77,16 +91,25 @@ class TestBatchMarkRead(unittest.TestCase):
|
|
|
77
91
|
|
|
78
92
|
@classmethod
|
|
79
93
|
def tearDownClass(cls):
|
|
94
|
+
# LOUD on failure — the old except:pass (plus never checking sb_rpc's
|
|
95
|
+
# returned error dict) let a 404 on mc_unregister_agent leak mread-*
|
|
96
|
+
# fixtures into the roster forever (incident 2026-06-10).
|
|
80
97
|
from meshcode.meshcode_mcp import backend as be
|
|
98
|
+
failures = []
|
|
81
99
|
for name in [AGENT_A, AGENT_B]:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
result = be.sb_rpc("mc_unregister_agent", {
|
|
101
|
+
"p_api_key": API_KEY,
|
|
102
|
+
"p_project_id": cls.pid,
|
|
103
|
+
"p_agent_name": name,
|
|
104
|
+
})
|
|
105
|
+
# sb_rpc reports errors as a return VALUE, not an exception.
|
|
106
|
+
if isinstance(result, dict) and (result.get("error") or result.get("_error")):
|
|
107
|
+
failures.append(f"{name}: {result}")
|
|
108
|
+
if failures:
|
|
109
|
+
raise AssertionError(
|
|
110
|
+
"tearDown could not unregister test agents — they are now "
|
|
111
|
+
f"ORPHANED in '{PROJECT}' and must be removed by hand:\n "
|
|
112
|
+
+ "\n ".join(failures))
|
|
90
113
|
|
|
91
114
|
def test_01_batch_mark_read(self):
|
|
92
115
|
"""Send 5 messages, read_inbox marks all read in one call."""
|
|
@@ -129,7 +152,7 @@ class TestBatchMarkRead(unittest.TestCase):
|
|
|
129
152
|
self.assertEqual(count, 0, f"Pending should be 0 after mark_read, got {count}")
|
|
130
153
|
|
|
131
154
|
|
|
132
|
-
@
|
|
155
|
+
@_LIVE_GATE
|
|
133
156
|
class TestEffectiveStatusIntegration(unittest.TestCase):
|
|
134
157
|
"""Live integration test for mc_agent_effective_status."""
|
|
135
158
|
|
|
@@ -17,10 +17,17 @@ import time
|
|
|
17
17
|
import unittest
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
20
22
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
21
23
|
|
|
22
24
|
os.environ.setdefault("SUPABASE_URL", "https://gjinagyyjttyxnaoavnz.supabase.co")
|
|
23
25
|
|
|
26
|
+
# Sends real messages (incl. impersonation probes a human can see on the
|
|
27
|
+
# dashboard) against the REAL backend — opt-in only (incident 2026-06-10).
|
|
28
|
+
# Deselect in CI/smoke with: -m "not live_mesh"
|
|
29
|
+
pytestmark = pytest.mark.live_mesh
|
|
30
|
+
|
|
24
31
|
|
|
25
32
|
def _get_api_key():
|
|
26
33
|
key = os.environ.get("MESHCODE_API_KEY")
|
|
@@ -35,11 +42,18 @@ def _get_api_key():
|
|
|
35
42
|
|
|
36
43
|
|
|
37
44
|
API_KEY = _get_api_key()
|
|
38
|
-
#
|
|
39
|
-
|
|
45
|
+
# NEVER default to a live mesh (the old hardcoded "mesh-dev" sent the
|
|
46
|
+
# impersonation probes of this file to the production dashboard). Unset =
|
|
47
|
+
# tests SKIP.
|
|
48
|
+
OWN_PROJECT = os.environ.get("MESHCODE_TEST_PROJECT")
|
|
40
49
|
OWN_PROJECT_ID = None
|
|
41
50
|
FAKE_PROJECT_ID = "00000000-0000-0000-0000-000000000000"
|
|
42
51
|
|
|
52
|
+
_LIVE_GATE = unittest.skipUnless(
|
|
53
|
+
API_KEY and OWN_PROJECT,
|
|
54
|
+
"MESHCODE_TEST_PROJECT (and an API key) required — live-mesh tests are "
|
|
55
|
+
"opt-in and must point at a dedicated test meshwork (incident 2026-06-10)")
|
|
56
|
+
|
|
43
57
|
|
|
44
58
|
def _resolve_project():
|
|
45
59
|
global OWN_PROJECT_ID
|
|
@@ -57,7 +71,7 @@ def _resolve_project():
|
|
|
57
71
|
return OWN_PROJECT_ID
|
|
58
72
|
|
|
59
73
|
|
|
60
|
-
@
|
|
74
|
+
@_LIVE_GATE
|
|
61
75
|
class TestCrossTenantIsolation(unittest.TestCase):
|
|
62
76
|
"""Agent in project X cannot access project Y data."""
|
|
63
77
|
|
|
@@ -135,7 +149,7 @@ class TestCrossTenantIsolation(unittest.TestCase):
|
|
|
135
149
|
self.assertTrue(is_safe, f"Unexpected result: {result}")
|
|
136
150
|
|
|
137
151
|
|
|
138
|
-
@
|
|
152
|
+
@_LIVE_GATE
|
|
139
153
|
class TestImpersonation(unittest.TestCase):
|
|
140
154
|
"""Cannot impersonate another agent or user."""
|
|
141
155
|
|
|
@@ -202,7 +216,7 @@ class TestImpersonation(unittest.TestCase):
|
|
|
202
216
|
f"Empty API key should be rejected: {result}")
|
|
203
217
|
|
|
204
218
|
|
|
205
|
-
@
|
|
219
|
+
@_LIVE_GATE
|
|
206
220
|
class TestRPCErrorConsistency(unittest.TestCase):
|
|
207
221
|
"""All RPCs should return consistent error format."""
|
|
208
222
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session-Replay tool_call Gate Tests (task 7f016375, DB F2.1)
|
|
3
|
+
=============================================================
|
|
4
|
+
tool_call events are 82% of mc_session_events storage; their only consumer
|
|
5
|
+
(session replay / timeline) is feature-flagged. These tests pin the gate:
|
|
6
|
+
|
|
7
|
+
1. server.py: _record_event_bg drops 'tool_call' while ff_session_replay
|
|
8
|
+
is off, records it when on, and NEVER gates lifecycle events.
|
|
9
|
+
2. The flag lookup is cached (TTL) and fail-closed on first error.
|
|
10
|
+
3. mig478 SQL: mc_record_event gates ONLY tool_call; the flag seeds
|
|
11
|
+
disabled; the agent-facing flag RPC exists with grants.
|
|
12
|
+
4. The gate is a FLAG, not sampling — no random() anywhere in the path.
|
|
13
|
+
|
|
14
|
+
Unit tests run fully offline (backend mocked). SQL checks parse migrations
|
|
15
|
+
(pattern of test_security_regressions.py).
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
pytest tests/test_session_replay_gate.py -v
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from unittest.mock import patch
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
28
|
+
|
|
29
|
+
MIGRATIONS_DIR = Path(__file__).parent.parent / "supabase" / "migrations"
|
|
30
|
+
MIG478 = MIGRATIONS_DIR / "20260610_478_tool_call_replay_gate.sql"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _fresh_server():
|
|
34
|
+
"""Import server with a clean flag cache. server.py is import-heavy but
|
|
35
|
+
side-effect-free at module level (asserted by test_boot_bug_regression)."""
|
|
36
|
+
from meshcode.meshcode_mcp import server
|
|
37
|
+
server._ff_replay_cache["enabled"] = None
|
|
38
|
+
server._ff_replay_cache["checked_mono"] = 0.0
|
|
39
|
+
return server
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _run_record(server, event_type, flag_response):
|
|
43
|
+
"""Call _record_event_bg with backend mocked; run the bg thread inline by
|
|
44
|
+
joining all threads it spawns. Returns the list of record_event calls."""
|
|
45
|
+
calls = []
|
|
46
|
+
import threading
|
|
47
|
+
started = []
|
|
48
|
+
orig_start = threading.Thread.start
|
|
49
|
+
|
|
50
|
+
def capture_start(self):
|
|
51
|
+
started.append(self)
|
|
52
|
+
orig_start(self)
|
|
53
|
+
|
|
54
|
+
with patch.object(server.be, "record_event",
|
|
55
|
+
side_effect=lambda *a, **k: calls.append(a)), \
|
|
56
|
+
patch.object(server.be, "feature_flag_enabled",
|
|
57
|
+
side_effect=flag_response), \
|
|
58
|
+
patch.object(server, "_get_api_key", return_value="mc_test_key"), \
|
|
59
|
+
patch.object(threading.Thread, "start", capture_start):
|
|
60
|
+
server._record_event_bg(event_type, {"tool": "x"})
|
|
61
|
+
for t in started:
|
|
62
|
+
t.join(timeout=5)
|
|
63
|
+
return calls
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestClientGate:
|
|
67
|
+
def test_tool_call_dropped_when_flag_off(self):
|
|
68
|
+
server = _fresh_server()
|
|
69
|
+
calls = _run_record(server, "tool_call",
|
|
70
|
+
lambda *a: {"ok": True, "enabled": False})
|
|
71
|
+
assert calls == [], "tool_call recorded despite ff_session_replay off"
|
|
72
|
+
|
|
73
|
+
def test_tool_call_recorded_when_flag_on(self):
|
|
74
|
+
server = _fresh_server()
|
|
75
|
+
calls = _run_record(server, "tool_call",
|
|
76
|
+
lambda *a: {"ok": True, "enabled": True})
|
|
77
|
+
assert len(calls) == 1 and calls[0][4] == "tool_call"
|
|
78
|
+
|
|
79
|
+
def test_lifecycle_events_never_gated(self):
|
|
80
|
+
for ev in ("boot", "shutdown", "error", "status_change"):
|
|
81
|
+
server = _fresh_server()
|
|
82
|
+
|
|
83
|
+
def explode(*a):
|
|
84
|
+
raise AssertionError(f"flag checked for lifecycle event {ev!r}")
|
|
85
|
+
calls = _run_record(server, ev, explode)
|
|
86
|
+
assert len(calls) == 1 and calls[0][4] == ev, (
|
|
87
|
+
f"lifecycle event {ev!r} must record unconditionally")
|
|
88
|
+
|
|
89
|
+
def test_fail_closed_on_first_flag_error(self):
|
|
90
|
+
server = _fresh_server()
|
|
91
|
+
|
|
92
|
+
def boom(*a):
|
|
93
|
+
raise OSError("network down")
|
|
94
|
+
calls = _run_record(server, "tool_call", boom)
|
|
95
|
+
assert calls == [], "flag fetch failure must fail CLOSED (no record)"
|
|
96
|
+
|
|
97
|
+
def test_flag_cached_within_ttl(self):
|
|
98
|
+
server = _fresh_server()
|
|
99
|
+
fetches = []
|
|
100
|
+
|
|
101
|
+
def counting(*a):
|
|
102
|
+
fetches.append(1)
|
|
103
|
+
return {"ok": True, "enabled": True}
|
|
104
|
+
_run_record(server, "tool_call", counting)
|
|
105
|
+
_run_record(server, "tool_call", counting)
|
|
106
|
+
assert len(fetches) == 1, (
|
|
107
|
+
f"flag fetched {len(fetches)}x within TTL — cache not working")
|
|
108
|
+
|
|
109
|
+
def test_no_sampling_anywhere(self):
|
|
110
|
+
"""Commander condition 1: flag gate, NOT sampling."""
|
|
111
|
+
import inspect
|
|
112
|
+
from meshcode.meshcode_mcp import server
|
|
113
|
+
src = inspect.getsource(server._record_event_bg) + \
|
|
114
|
+
inspect.getsource(server._session_replay_enabled)
|
|
115
|
+
assert "random" not in src
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestMig478Sql:
|
|
119
|
+
SQL = None
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def setup_class(cls):
|
|
123
|
+
cls.SQL = MIG478.read_text(errors="replace")
|
|
124
|
+
|
|
125
|
+
def test_gate_only_wraps_tool_call(self):
|
|
126
|
+
body = re.search(r"FUNCTION public\.mc_record_event.*?\$\$;", self.SQL,
|
|
127
|
+
re.DOTALL).group(0)
|
|
128
|
+
gate = re.search(r"IF p_event_type = 'tool_call' THEN(.*?)END IF;",
|
|
129
|
+
body, re.DOTALL)
|
|
130
|
+
assert gate, "tool_call gate missing from mc_record_event"
|
|
131
|
+
assert "ff_session_replay" in gate.group(1)
|
|
132
|
+
# The INSERT must be OUTSIDE the gate so other event types still write.
|
|
133
|
+
after_gate = body[body.index(gate.group(0)) + len(gate.group(0)):]
|
|
134
|
+
assert "INSERT INTO meshcode.mc_session_events" in after_gate
|
|
135
|
+
|
|
136
|
+
def test_flag_seeds_disabled(self):
|
|
137
|
+
seed = re.search(r"INSERT INTO meshcode\.mc_global_config.*?ON CONFLICT",
|
|
138
|
+
self.SQL, re.DOTALL).group(0)
|
|
139
|
+
assert "'ff_session_replay'" in seed
|
|
140
|
+
assert '"enabled": false' in seed
|
|
141
|
+
assert "DO NOTHING" in self.SQL.split("ON CONFLICT", 1)[1][:30]
|
|
142
|
+
|
|
143
|
+
def test_flag_rpc_exists_with_grants(self):
|
|
144
|
+
assert "FUNCTION public.mc_feature_flag_enabled" in self.SQL
|
|
145
|
+
assert re.search(
|
|
146
|
+
r"GRANT EXECUTE ON FUNCTION public\.mc_feature_flag_enabled.*?TO anon",
|
|
147
|
+
self.SQL)
|
|
148
|
+
|
|
149
|
+
def test_backend_wrapper_targets_new_rpc(self):
|
|
150
|
+
from meshcode.meshcode_mcp import backend as be
|
|
151
|
+
import inspect
|
|
152
|
+
src = inspect.getsource(be.feature_flag_enabled)
|
|
153
|
+
assert "mc_feature_flag_enabled" in src
|
|
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
|
|
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
|
|
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
|
|
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
|