meshcode 2.11.128__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.128 → meshcode-2.11.129}/PKG-INFO +2 -11
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/__init__.py +1 -1
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/backend.py +11 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/server.py +43 -6
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/secrets.py +51 -11
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode.egg-info/PKG-INFO +2 -11
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode.egg-info/SOURCES.txt +1 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/pyproject.toml +1 -1
- meshcode-2.11.129/tests/test_session_replay_gate.py +153 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/README.md +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/__main__.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/cli.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/compat.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/daemon.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/doctor.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/hostd.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/invites.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/launcher.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/swarm.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/meshcode_mcp/test_swarm.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/preferences.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/run_agent.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/self_update.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/up.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode/upload.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/setup.cfg +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_core.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_doctor.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_live_mesh_guard.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_stop_ghost_terminal.py +0 -0
- {meshcode-2.11.128 → meshcode-2.11.129}/tests/test_swarm_events.py +0 -0
- {meshcode-2.11.128 → 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
|
|
|
@@ -87,6 +87,7 @@ tests/test_run_agent_no_server_import.py
|
|
|
87
87
|
tests/test_security_regressions.py
|
|
88
88
|
tests/test_self_update_user_site.py
|
|
89
89
|
tests/test_sentinel.py
|
|
90
|
+
tests/test_session_replay_gate.py
|
|
90
91
|
tests/test_setup_path.py
|
|
91
92
|
tests/test_sleep_signals.py
|
|
92
93
|
tests/test_status_enum_coverage.py
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|