meshcode 2.11.152__tar.gz → 2.11.154__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 (116) hide show
  1. {meshcode-2.11.152 → meshcode-2.11.154}/PKG-INFO +1 -1
  2. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/__init__.py +1 -1
  3. meshcode-2.11.154/meshcode/_launch_smoke.py +227 -0
  4. meshcode-2.11.154/meshcode/_update_guard.py +175 -0
  5. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/comms_v4.py +53 -11
  6. meshcode-2.11.154/meshcode/hooks/push_guard.py +208 -0
  7. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/hooks/repo_path_lock.py +52 -2
  8. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/hostd.py +43 -0
  9. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/server.py +122 -24
  10. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/protocol_handler.py +60 -27
  11. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/run_agent.py +222 -26
  12. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/self_update.py +12 -0
  13. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/up.py +32 -6
  14. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode.egg-info/PKG-INFO +1 -1
  15. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode.egg-info/SOURCES.txt +9 -0
  16. {meshcode-2.11.152 → meshcode-2.11.154}/pyproject.toml +1 -1
  17. meshcode-2.11.154/tests/test_launch_smoke.py +87 -0
  18. meshcode-2.11.154/tests/test_preflight_hb_gate.py +42 -0
  19. meshcode-2.11.154/tests/test_push_guard.py +123 -0
  20. meshcode-2.11.154/tests/test_rm_guard.py +56 -0
  21. meshcode-2.11.154/tests/test_up_launch_cmd.py +31 -0
  22. meshcode-2.11.154/tests/test_update_guard.py +96 -0
  23. {meshcode-2.11.152 → meshcode-2.11.154}/README.md +0 -0
  24. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/__main__.py +0 -0
  25. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/_session_handoff_template.py +0 -0
  26. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/_stop_hook_template.py +0 -0
  27. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/ascii_art.py +0 -0
  28. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/atomic_push.py +0 -0
  29. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/claude_update.py +0 -0
  30. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/cli.py +0 -0
  31. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/compat.py +0 -0
  32. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/daemon.py +0 -0
  33. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/date_parse.py +0 -0
  34. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/doctor.py +0 -0
  35. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/error_hints.py +0 -0
  36. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/exceptions.py +0 -0
  37. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/helper_visuals.py +0 -0
  38. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/hooks/__init__.py +0 -0
  39. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/invites.py +0 -0
  40. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/launcher.py +0 -0
  41. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/launcher_install.py +0 -0
  42. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/__init__.py +0 -0
  43. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/__main__.py +0 -0
  44. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/backend.py +0 -0
  45. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/realtime.py +0 -0
  46. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  47. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/swarm.py +0 -0
  48. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_backend.py +0 -0
  49. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  50. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  51. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  52. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  53. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  54. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_swarm.py +0 -0
  55. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/preferences.py +0 -0
  56. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/protocol_v2.py +0 -0
  57. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/quickstart.py +0 -0
  58. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/rpc_allowlist.py +0 -0
  59. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/scripts/check_secrets.py +0 -0
  60. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/scripts/race_rate_harness.py +0 -0
  61. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/secrets.py +0 -0
  62. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/setup_clients.py +0 -0
  63. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/supervisor.py +0 -0
  64. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/upload.py +0 -0
  65. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode.egg-info/dependency_links.txt +0 -0
  66. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode.egg-info/entry_points.txt +0 -0
  67. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode.egg-info/requires.txt +0 -0
  68. {meshcode-2.11.152 → meshcode-2.11.154}/meshcode.egg-info/top_level.txt +0 -0
  69. {meshcode-2.11.152 → meshcode-2.11.154}/setup.cfg +0 -0
  70. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_auto_update_hardening.py +0 -0
  71. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_autonomous_closegap_1.py +0 -0
  72. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_autonomous_closegap_2.py +0 -0
  73. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_autonomous_closegap_3.py +0 -0
  74. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_autonomous_prompt_inject.py +0 -0
  75. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_boot_bug_regression.py +0 -0
  76. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_color_truecolor.py +0 -0
  77. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_core.py +0 -0
  78. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_cross_agent_messaging.py +0 -0
  79. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_date_parse.py +0 -0
  80. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_doctor.py +0 -0
  81. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_epistemic_v1_python_sdk.py +0 -0
  82. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  83. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_esc_deaf_state.py +0 -0
  84. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_exceptions.py +0 -0
  85. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_file_upload.py +0 -0
  86. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_helper_visuals.py +0 -0
  87. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_hostd_launch_pinned_env.py +0 -0
  88. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_hostd_serve_discovery_split.py +0 -0
  89. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_hostd_zombie_sessions.py +0 -0
  90. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_init_device_code.py +0 -0
  91. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_install_guard.py +0 -0
  92. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_lease_sigterm_release.py +0 -0
  93. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_live_mesh_guard.py +0 -0
  94. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_mark_read_batch.py +0 -0
  95. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_marketplace_ratings.py +0 -0
  96. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_migration_integrity.py +0 -0
  97. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_pretrust_claude.py +0 -0
  98. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_realtime_event_freshness.py +0 -0
  99. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_rls_cross_tenant.py +0 -0
  100. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_rpc_grants.py +0 -0
  101. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_rpc_migrations.py +0 -0
  102. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_run_agent_dry_run.py +0 -0
  103. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_run_agent_no_server_import.py +0 -0
  104. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_security_regressions.py +0 -0
  105. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_self_update_user_site.py +0 -0
  106. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_sentinel.py +0 -0
  107. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_session_replay_gate.py +0 -0
  108. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_setup_path.py +0 -0
  109. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_sleep_signals.py +0 -0
  110. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_status_enum_coverage.py +0 -0
  111. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_stay_on_loop_hook.py +0 -0
  112. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_stop_ghost_terminal.py +0 -0
  113. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_swarm_events.py +0 -0
  114. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_task_progress.py +0 -0
  115. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_terminal_lifecycle.py +0 -0
  116. {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_wait_open_tasks_contradiction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.152
3
+ Version: 2.11.154
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,5 +1,5 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.11.152"
2
+ __version__ = "2.11.154"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -0,0 +1,227 @@
1
+ """Golden launch smoke — Layer 1 of Launch Regression Armor (task 2e85c6a8).
2
+
3
+ Deterministic, offline-capable proof that a launch path is HEALTHY. It exists so a
4
+ broken launch is caught at two gates instead of in Samuel's terminal:
5
+
6
+ * Layer 2 (pre-publish CI gate) runs the OFFLINE subset against a freshly built
7
+ wheel — a wheel that fails smoke never reaches PyPI.
8
+ * Layer 3 (runtime update-guard) runs the OFFLINE subset against an on-disk env
9
+ BEFORE hostd execs into it — a bad version is refused, not adopted.
10
+
11
+ OFFLINE subset (no creds, no live mesh — the part CI/runtime-guard rely on):
12
+
13
+ version_coherence
14
+ meshcode.__version__ (the loaded module) must equal the installed dist
15
+ metadata version, and — if the launcher stamped it — MESHCODE_EXPECTED_VERSION.
16
+ A mismatch IS the runtime version-split (user-site vs pinned env, or an
17
+ egg-info source shadow) that produced Samuel's doubles + loop-drop. This is
18
+ THE root cause this armor exists for, so it is check #1.
19
+
20
+ mcp_import
21
+ meshcode.meshcode_mcp.server must import cleanly in an isolated subprocess in
22
+ under the MCP handshake budget (2.0 s). Catches import-time sys.exit
23
+ (BUG-COMMANDER-MCP-BOOT) and slow-import regressions that silently drop the
24
+ meshcode_* tools past Claude Code's 5 s MCP_TIMEOUT. Mirrors the env + parse
25
+ contract of meshcode_mcp/test_boot_timing.py (single source of the budget).
26
+
27
+ LIVE subset (--live; needs creds + a resolvable agent), additive on top of offline:
28
+
29
+ doctor doctor.run_doctor() reports no hard failures.
30
+ dry_run_launch `meshcode run <project>/<agent> --dry-run` exits 0 ("DRY-RUN OK"):
31
+ the full bootstrap codepath (workspace, .mcp.json, ensure_boot_env,
32
+ ownership, editor detection, cmd construction) short of execvp.
33
+
34
+ CLI: meshcode smoke [--live] [--json] [project/agent]
35
+ exit 0 iff every selected check passes. --json emits a machine-readable report
36
+ {ok, checks:[{check, ok, detail}, ...]} for the CI gate to parse.
37
+ """
38
+ from __future__ import annotations
39
+
40
+ import json
41
+ import os
42
+ import subprocess
43
+ import sys
44
+ from typing import Optional
45
+
46
+ # MCP handshake budget. Claude Code's default MCP_TIMEOUT is 5 s; a 2 s import ceiling
47
+ # leaves room for run_server() + lifespan pre-yield work. Kept in lock-step with
48
+ # meshcode_mcp/test_boot_timing.py:MAX_IMPORT_MS (2000).
49
+ BOOT_IMPORT_BUDGET_S = 2.0
50
+
51
+
52
+ def _check_version_coherence() -> dict:
53
+ """Loaded __version__ == installed dist metadata == MESHCODE_EXPECTED_VERSION.
54
+
55
+ Returns N/A-pass when the dist metadata is absent (source/editable checkout) so
56
+ dev runs don't false-positive; a mismatch when BOTH are present is the version
57
+ split we are hunting and fails hard.
58
+ """
59
+ try:
60
+ import meshcode
61
+ loaded = getattr(meshcode, "__version__", None)
62
+ except Exception as e: # pragma: no cover - import of own pkg failing is itself a fail
63
+ return {"check": "version_coherence", "ok": False,
64
+ "detail": f"import meshcode failed: {e}"}
65
+
66
+ installed: Optional[str] = None
67
+ try:
68
+ from importlib.metadata import version as _dist_version
69
+ installed = _dist_version("meshcode")
70
+ except Exception:
71
+ installed = None # source/editable run — no dist metadata to compare against
72
+
73
+ expected = (os.environ.get("MESHCODE_EXPECTED_VERSION") or "").strip() or None
74
+
75
+ mismatches = []
76
+ if installed and loaded and installed != loaded:
77
+ mismatches.append(f"loaded={loaded} != installed_dist={installed}")
78
+ if expected and loaded and expected != loaded:
79
+ mismatches.append(f"loaded={loaded} != expected_env={expected}")
80
+
81
+ ok = not mismatches and bool(loaded)
82
+ if not loaded:
83
+ detail = "meshcode.__version__ missing"
84
+ elif mismatches:
85
+ detail = "; ".join(mismatches)
86
+ else:
87
+ detail = f"loaded={loaded} installed={installed or 'n/a(source)'} expected={expected or 'unset'}"
88
+ return {"check": "version_coherence", "ok": ok, "detail": detail}
89
+
90
+
91
+ def _check_mcp_import() -> dict:
92
+ """Import meshcode.meshcode_mcp.server in a clean subprocess; assert it succeeds
93
+ fast. Env + IMPORT_MS parse contract mirror test_boot_timing.py exactly."""
94
+ env = os.environ.copy()
95
+ # Stable identity so the module-load path doesn't enter the slow mc_resolve_project
96
+ # retry loop; these mirror what `meshcode setup` bakes into a real .mcp.json.
97
+ env.setdefault("MESHCODE_PROJECT", "launch-smoke")
98
+ env.setdefault("MESHCODE_PROJECT_ID", "00000000-0000-0000-0000-000000000000")
99
+ env.setdefault("MESHCODE_AGENT", "launch-smoke")
100
+ env.setdefault("MESHCODE_KEYCHAIN_PROFILE", "default")
101
+ env.setdefault("SUPABASE_URL", "https://gjinagyyjttyxnaoavnz.supabase.co")
102
+ env.setdefault("SUPABASE_KEY", "sb_publishable_qwN9PO1L7jUXhhbhhVk2CQ_z1FXG2Qf")
103
+ # Skip the PyPI auto-update HTTP GET — orthogonal to import latency.
104
+ env["MESHCODE_AUTO_UPDATE"] = "0"
105
+
106
+ # server.py redirects sys.stdout->stderr at load to protect the JSON-RPC channel,
107
+ # so write the marker via os.write(1, ...) and check both buffers (per test_boot_timing).
108
+ snippet = (
109
+ "import time, sys, os\n"
110
+ "t0 = time.monotonic_ns()\n"
111
+ "import meshcode.meshcode_mcp.server # noqa: F401\n"
112
+ "elapsed_ms = (time.monotonic_ns() - t0) / 1_000_000\n"
113
+ "os.write(1, f'IMPORT_MS={elapsed_ms:.0f}\\n'.encode())\n"
114
+ )
115
+ try:
116
+ result = subprocess.run(
117
+ [sys.executable, "-c", snippet],
118
+ env=env, capture_output=True, text=True, timeout=30,
119
+ )
120
+ except subprocess.TimeoutExpired:
121
+ return {"check": "mcp_import", "ok": False, "detail": "import timed out >30s"}
122
+
123
+ elapsed_ms = None
124
+ for buf in (result.stdout, result.stderr):
125
+ for line in buf.splitlines():
126
+ if line.startswith("IMPORT_MS="):
127
+ try:
128
+ elapsed_ms = float(line.split("=", 1)[1])
129
+ except ValueError:
130
+ pass
131
+ break
132
+ if elapsed_ms is not None:
133
+ break
134
+
135
+ if result.returncode != 0 or elapsed_ms is None:
136
+ tail = "\n".join((result.stderr or result.stdout or "").splitlines()[-3:]).strip()
137
+ return {"check": "mcp_import", "ok": False,
138
+ "detail": f"import failed rc={result.returncode}: {tail or '(no output)'}"}
139
+
140
+ budget_ms = BOOT_IMPORT_BUDGET_S * 1000
141
+ ok = elapsed_ms < budget_ms
142
+ return {"check": "mcp_import", "ok": ok,
143
+ "detail": f"import {elapsed_ms:.0f}ms (budget {budget_ms:.0f}ms)"}
144
+
145
+
146
+ def _check_doctor() -> dict:
147
+ """Live: doctor.run_doctor() reports no hard failures (warnings allowed)."""
148
+ try:
149
+ from meshcode.doctor import run_doctor
150
+ rc = run_doctor(skip_api_key_check=False, quiet=True)
151
+ except Exception as e:
152
+ return {"check": "doctor", "ok": False, "detail": f"run_doctor raised: {e}"}
153
+ return {"check": "doctor", "ok": rc == 0,
154
+ "detail": "no hard failures" if rc == 0 else f"run_doctor rc={rc} (a check failed)"}
155
+
156
+
157
+ def _check_dry_run_launch(target: str) -> dict:
158
+ """Live: `meshcode run <target> --dry-run` exits 0 and prints DRY-RUN OK."""
159
+ try:
160
+ result = subprocess.run(
161
+ [sys.executable, "-m", "meshcode", "run", target, "--dry-run"],
162
+ capture_output=True, text=True, timeout=60,
163
+ )
164
+ except subprocess.TimeoutExpired:
165
+ return {"check": "dry_run_launch", "ok": False, "detail": f"{target}: dry-run timed out >60s"}
166
+ blob = (result.stdout or "") + (result.stderr or "")
167
+ ok = result.returncode == 0 and "DRY-RUN OK" in blob
168
+ if ok:
169
+ detail = f"{target}: DRY-RUN OK"
170
+ else:
171
+ tail = "\n".join(blob.splitlines()[-3:]).strip()
172
+ detail = f"{target}: rc={result.returncode} {tail or '(no DRY-RUN OK marker)'}"
173
+ return {"check": "dry_run_launch", "ok": ok, "detail": detail}
174
+
175
+
176
+ def run_smoke(live: bool = False, target: Optional[str] = None) -> dict:
177
+ """Run the smoke suite. Returns {ok, live, checks:[...]}.
178
+
179
+ Offline checks always run. `live` adds doctor + (if `target` resolvable) a
180
+ dry-run launch. `target` defaults to MESHCODE_PROJECT/MESHCODE_AGENT env when set.
181
+ """
182
+ checks = [_check_version_coherence(), _check_mcp_import()]
183
+
184
+ if live:
185
+ checks.append(_check_doctor())
186
+ tgt = target
187
+ if not tgt:
188
+ proj = (os.environ.get("MESHCODE_PROJECT") or "").strip()
189
+ agent = (os.environ.get("MESHCODE_AGENT") or "").strip()
190
+ if proj and agent:
191
+ tgt = f"{proj}/{agent}"
192
+ if tgt:
193
+ checks.append(_check_dry_run_launch(tgt))
194
+ else:
195
+ checks.append({"check": "dry_run_launch", "ok": True,
196
+ "detail": "skipped (no target; pass project/agent or set MESHCODE_PROJECT+AGENT)"})
197
+
198
+ ok = all(c["ok"] for c in checks)
199
+ return {"ok": ok, "live": live, "checks": checks}
200
+
201
+
202
+ def cmd_smoke(flags, pos) -> int:
203
+ """CLI entry: meshcode smoke [--live] [--json] [project/agent]."""
204
+ live = bool(flags.get("live")) or "--live" in sys.argv
205
+ json_out = bool(flags.get("json")) or "--json" in sys.argv
206
+ target = pos[0] if pos else None
207
+
208
+ report = run_smoke(live=live, target=target)
209
+
210
+ if json_out:
211
+ print(json.dumps(report))
212
+ else:
213
+ mark = {True: "PASS", False: "FAIL"}
214
+ print(f"\n meshcode launch smoke — {'LIVE' if live else 'offline'} subset\n")
215
+ for c in report["checks"]:
216
+ print(f" [{mark[c['ok']]}] {c['check']:<18} {c['detail']}")
217
+ print()
218
+ print(f" {'ALL CHECKS PASS' if report['ok'] else 'SMOKE FAILED'}\n")
219
+
220
+ return 0 if report["ok"] else 1
221
+
222
+
223
+ if __name__ == "__main__":
224
+ # Allow `python -m meshcode._launch_smoke [--live] [--json] [target]`.
225
+ _flags = {a.lstrip("-"): True for a in sys.argv[1:] if a.startswith("--")}
226
+ _pos = [a for a in sys.argv[1:] if not a.startswith("--")]
227
+ sys.exit(cmd_smoke(_flags, _pos))
@@ -0,0 +1,175 @@
1
+ """Runtime update-guard — Layer 3 of Launch Regression Armor (task 2e85c6a8).
2
+
3
+ Gates the two seams where meshcode adopts a NEW on-disk version at runtime — the
4
+ freshly built immutable env (self_update.ensure_versioned_env) and the hostd
5
+ version-drift restart — on the L1 golden smoke run against the CANDIDATE env. A
6
+ candidate that fails launch smoke is, under enforce, refused: the known-good version
7
+ keeps running and every mesh commander is alerted.
8
+
9
+ MESHCODE_UPDATE_GUARD = off | dry_run | enforce (default: off)
10
+
11
+ off guard_blocks_candidate() is always False — ZERO behavior change.
12
+ Ship dark, flip to dry_run via config (no recut).
13
+ dry_run smoke runs; a failing candidate is LOGGED + alerted (mode=dry_run),
14
+ but guard_blocks_candidate() returns False (caller proceeds). Pure
15
+ observability — the rollout step before enforce.
16
+ enforce a failing candidate makes guard_blocks_candidate() return True.
17
+
18
+ Hard invariants:
19
+ * NEVER raises into a caller. Any internal error -> allow (fail-open) so a guard
20
+ bug can never wedge a launch.
21
+ * A candidate that predates the `meshcode smoke` verb (no parseable verdict) ->
22
+ fail-open (allow) — the guard only enforces what it can actually measure.
23
+ * Results are TTL-cached per candidate version so a hot caller (hostd's sweep
24
+ loop) doesn't re-run smoke every cycle.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import os
30
+ import subprocess
31
+ import sys
32
+ import tempfile
33
+ import time
34
+ from typing import List, Optional, Tuple
35
+
36
+ _VALID_MODES = ("off", "dry_run", "enforce")
37
+ _SMOKE_TIMEOUT_S = 90
38
+ _CACHE_TTL_S = 300.0
39
+ # {to_version: (monotonic_ts, smoke_ok_or_None, failures)}
40
+ _CACHE: dict = {}
41
+
42
+
43
+ def guard_mode() -> str:
44
+ m = (os.environ.get("MESHCODE_UPDATE_GUARD") or "off").strip().lower()
45
+ return m if m in _VALID_MODES else "off"
46
+
47
+
48
+ def _log(msg: str) -> None:
49
+ try:
50
+ print(f"[meshcode][update-guard] {msg}", file=sys.stderr)
51
+ except Exception:
52
+ pass
53
+
54
+
55
+ def _run_smoke(candidate_py: str) -> Tuple[Optional[bool], List[str]]:
56
+ """Run `<candidate_py> -m meshcode smoke --json` from a SCRATCH cwd.
57
+
58
+ Scratch cwd is mandatory: running from a source/checkout tree makes the smoke's
59
+ version_coherence read the repo's stale egg-info metadata and false-fail (the
60
+ documented egg-info-shadow trap). Returns (ok, failures); ok is None when the
61
+ harness could not produce a verdict (treated as fail-open by the caller).
62
+ """
63
+ scratch = None
64
+ try:
65
+ scratch = tempfile.mkdtemp(prefix="mc-smoke-")
66
+ except Exception:
67
+ scratch = None
68
+ try:
69
+ r = subprocess.run(
70
+ [candidate_py, "-m", "meshcode", "smoke", "--json"],
71
+ capture_output=True, text=True, timeout=_SMOKE_TIMEOUT_S,
72
+ cwd=scratch or None,
73
+ )
74
+ except Exception:
75
+ return None, [] # couldn't run -> no verdict -> fail-open
76
+ finally:
77
+ if scratch:
78
+ try:
79
+ import shutil
80
+ shutil.rmtree(scratch, ignore_errors=True)
81
+ except Exception:
82
+ pass
83
+
84
+ report = None
85
+ for line in reversed((r.stdout or "").splitlines()):
86
+ line = line.strip()
87
+ if line.startswith("{"):
88
+ try:
89
+ report = json.loads(line)
90
+ break
91
+ except Exception:
92
+ continue
93
+ if not isinstance(report, dict) or "ok" not in report:
94
+ return None, [] # candidate has no smoke verb / unparseable -> fail-open
95
+ failures = [f"{c.get('check')}: {c.get('detail')}"
96
+ for c in report.get("checks", []) if not c.get("ok")]
97
+ return bool(report["ok"]), failures
98
+
99
+
100
+ def _cached_smoke(to_version: str, candidate_py: str) -> Tuple[Optional[bool], List[str]]:
101
+ now = time.monotonic()
102
+ hit = _CACHE.get(to_version)
103
+ if hit and (now - hit[0]) < _CACHE_TTL_S:
104
+ return hit[1], hit[2]
105
+ ok, failures = _run_smoke(candidate_py)
106
+ _CACHE[to_version] = (now, ok, failures)
107
+ return ok, failures
108
+
109
+
110
+ def _emit_alert(mode: str, from_version: str, to_version: str,
111
+ failures: List[str], context: str) -> None:
112
+ """Best-effort in-mesh alert (type version_guard_block) to mesh-commander.
113
+
114
+ Cross-mesh fan-out to EVERY mesh commander (Samuel "TODOS los commanders", Q3) is
115
+ the commander/server piece (pending the mc_alert_all_commanders-vs-iterate
116
+ decision). Here we always LOG (the durable DRY-RUN telemetry signal) and
117
+ best-effort send the in-mesh report — creds may be absent in a hostd context, in
118
+ which case the log line is the record.
119
+ """
120
+ payload = {
121
+ "type": "version_guard_block", "mode": mode,
122
+ "from_version": from_version, "to_version": to_version,
123
+ "reason": "smoke_fail", "smoke_failures": failures[:6], "context": context,
124
+ }
125
+ _log(f"ALERT version_guard_block mode={mode} {from_version}->{to_version} "
126
+ f"context={context} failures={failures[:3]}")
127
+ agent = os.environ.get("MESHCODE_AGENT") or "hostd"
128
+ try:
129
+ from meshcode import backend as _be
130
+ proj = os.environ.get("MESHCODE_PROJECT") or ""
131
+ if proj:
132
+ _be.send_message(proj, agent, "mesh-commander", payload, msg_type="report")
133
+ # Cross-mesh fan-out to EVERY mesh commander (Q3, Samuel "TODOS los
134
+ # commanders"). database task 0cdc6646: mc_alert_all_commanders enumerates
135
+ # the commander of each ACTIVE-linked mesh + the local one. Best-effort —
136
+ # needs an agent api key (may be absent in a bare hostd ctx); the in-mesh
137
+ # report + log above are the fallback signal.
138
+ _key = os.environ.get("MESHCODE_API_KEY") or ""
139
+ if _key:
140
+ _be.sb_rpc("mc_alert_all_commanders", {
141
+ "p_api_key": _key,
142
+ "p_payload": payload,
143
+ "p_from_agent": agent,
144
+ "p_type": "version_guard_block",
145
+ })
146
+ except Exception:
147
+ pass # best-effort; the log line above is the durable signal
148
+
149
+
150
+ def guard_blocks_candidate(candidate_py: str, *, to_version: str,
151
+ from_version: str = "?", context: str = "") -> bool:
152
+ """Return True iff the caller MUST refuse this candidate version.
153
+
154
+ True only when mode==enforce AND smoke gives a definitive ok==False. off ->
155
+ False without running smoke. dry_run -> runs smoke + alerts on failure but
156
+ returns False. Fail-open on any ambiguity (no verdict / internal error).
157
+ """
158
+ try:
159
+ mode = guard_mode()
160
+ if mode == "off":
161
+ return False
162
+ ok, failures = _cached_smoke(to_version, candidate_py)
163
+ if ok is None:
164
+ _log(f"no smoke verdict for {to_version} (context={context}) -> fail-open (allow)")
165
+ return False
166
+ if ok:
167
+ return False
168
+ _emit_alert(mode, from_version, to_version, failures, context)
169
+ if mode == "enforce":
170
+ _log(f"BLOCK {from_version}->{to_version} (context={context}); keeping known-good.")
171
+ return True
172
+ _log(f"DRY-RUN would-block {from_version}->{to_version} (context={context}); proceeding.")
173
+ return False
174
+ except Exception:
175
+ return False # the guard must NEVER wedge a caller
@@ -3322,21 +3322,31 @@ if __name__ == "__main__":
3322
3322
 
3323
3323
  elif cmd in ("kill", "wake", "sleep"):
3324
3324
  # meshcode kill|wake|sleep <project> <agent>
3325
+ # F3 (task f54a0c30): the old mc_agent_kill(p_project_id,p_agent_name) carried
3326
+ # NO p_api_key, so it could not authenticate the OWNER -> "Permission denied".
3327
+ # Route through the api-key-authenticated power RPC (mig581
3328
+ # mc_agent_power_by_api_key) so the caller's key proves ownership.
3329
+ # kill/sleep -> desired_state='stopped'; wake -> 'running' (+restart).
3325
3330
  proj = pos[0] if len(pos) > 0 else "default"
3326
3331
  name = pos[1] if len(pos) > 1 else ""
3327
3332
  if not name:
3328
3333
  print(f"[meshcode] ERROR: Usage: meshcode {cmd} <project> <agent>")
3329
3334
  sys.exit(1)
3330
- project_id = get_project_id(proj)
3331
- if not project_id:
3332
- print(f"[meshcode] ERROR: project '{proj}' not found")
3335
+ _ak = _load_api_key_for_cli()
3336
+ if not _ak:
3337
+ print("[meshcode] ERROR: not logged in (run: meshcode login <api_key>)")
3333
3338
  sys.exit(1)
3334
- rpc_name = {"kill": "mc_agent_kill", "wake": "mc_agent_wake", "sleep": "mc_agent_sleep"}[cmd]
3335
- result = sb_rpc(rpc_name, {"p_project_id": project_id, "p_agent_name": name})
3336
- if result and result.get("error"):
3337
- print(f"[meshcode] ERROR: {result['error']}")
3339
+ _state = "running" if cmd == "wake" else "stopped"
3340
+ _params = {"p_api_key": _ak, "p_project": proj, "p_agent": name, "p_state": _state}
3341
+ if cmd == "wake":
3342
+ _params["p_restart"] = True
3343
+ result = sb_rpc("mc_agent_power_by_api_key", _params)
3344
+ if not isinstance(result, dict) or result.get("error") or not result.get("ok", True):
3345
+ _msg = ((result or {}).get("error") or (result or {}).get("detail") or "unknown error") \
3346
+ if isinstance(result, dict) else "no response from server"
3347
+ print(f"[meshcode] ERROR: {cmd} failed: {_msg}")
3338
3348
  sys.exit(1)
3339
- print(f"[{proj}] {name}: {cmd} → {json.dumps(result, ensure_ascii=False)}")
3349
+ print(f"[{proj}] {name}: {cmd} → desired_state={_state}")
3340
3350
 
3341
3351
  elif cmd == "profile":
3342
3352
  # meshcode profile get|set <project> <agent> [--color X --display-name Y --role Z --launch-prompt P]
@@ -3404,7 +3414,7 @@ if __name__ == "__main__":
3404
3414
  proj, base = base.split("/", 1)
3405
3415
  if not base:
3406
3416
  print("[meshcode] ERROR: usage: meshcode replicate <agent> --count N "
3407
- "[--project <name>] [--no-launch]")
3417
+ "[--project <name>] [--no-launch] [--mode copilotos|independientes]")
3408
3418
  sys.exit(1)
3409
3419
  try:
3410
3420
  count = int(flags.get("count", pos[1] if len(pos) > 1 else 0))
@@ -3414,6 +3424,14 @@ if __name__ == "__main__":
3414
3424
  print("[meshcode] ERROR: --count must be an integer in 1..16")
3415
3425
  sys.exit(1)
3416
3426
  desired = None if flags.get("no-launch") else "running"
3427
+ # p_mode (task 986947b5): copilotos = shared swarm_id (coordinate via one tray) |
3428
+ # independientes = distinct swarm_id per replica (isolated). Default copilotos
3429
+ # (Samuel's canonical spec). DB mig586 honors it on both overloads.
3430
+ mode = str(flags.get("mode", "copilotos")).lower()
3431
+ if mode not in ("copilotos", "independientes"):
3432
+ print("[meshcode] ERROR: --mode must be 'copilotos' (shared/coordinate) or "
3433
+ "'independientes' (isolated)")
3434
+ sys.exit(1)
3417
3435
  res = sb_rpc("mc_replicate_agent", {
3418
3436
  "p_api_key": _ak,
3419
3437
  "p_base_agent": base,
@@ -3421,10 +3439,25 @@ if __name__ == "__main__":
3421
3439
  "p_project": proj,
3422
3440
  "p_desired_state": desired,
3423
3441
  "p_replica_group_id": flags.get("group"),
3442
+ "p_mode": mode,
3424
3443
  })
3425
3444
  if not isinstance(res, dict) or res.get("error") or not res.get("ok", True):
3426
- _err = (res or {}).get("error", "unknown") if isinstance(res, dict) else "no response"
3427
- print(f"[meshcode] ERROR: replicate failed: {_err}")
3445
+ # F1 (task f54a0c30): the RPC puts the actionable text in `detail` /
3446
+ # `error_code`, but the old handler read only `error` -> "unknown".
3447
+ # Surface error_code + detail, with a concrete fix for ambiguous_project.
3448
+ if not isinstance(res, dict):
3449
+ print("[meshcode] ERROR: replicate failed: no response from server")
3450
+ elif res.get("error_code") == "ambiguous_project":
3451
+ print(f"[meshcode] ERROR: replicate: base agent '{base}' exists in "
3452
+ f"multiple projects — disambiguate with:")
3453
+ print(f" meshcode replicate <project>/{base} --count {count}"
3454
+ + ("" if desired == "running" else " --no-launch")
3455
+ + " (or pass --project <name>)")
3456
+ else:
3457
+ _ec = res.get("error_code")
3458
+ _msg = res.get("error") or res.get("detail") or "unknown error"
3459
+ print("[meshcode] ERROR: replicate failed"
3460
+ + (f" [{_ec}]" if _ec else "") + f": {_msg}")
3428
3461
  sys.exit(1)
3429
3462
  created = res.get("created") or []
3430
3463
  print(f"[meshcode] Replicated {base} → {len(created)} replica(s) "
@@ -3994,6 +4027,15 @@ if __name__ == "__main__":
3994
4027
  elif cmd == "doctor":
3995
4028
  cmd_doctor(flags, pos)
3996
4029
 
4030
+ elif cmd == "smoke":
4031
+ # Golden launch smoke (Layer 1 of launch-armor, task 2e85c6a8).
4032
+ # Offline subset (version coherence + mcp import) uses no backend; --live
4033
+ # adds doctor + dry-run launch. The Layer-2 CI gate and Layer-3 runtime
4034
+ # update-guard invoke this against a fresh wheel / on-disk env.
4035
+ import importlib
4036
+ _smoke = importlib.import_module("meshcode._launch_smoke")
4037
+ sys.exit(_smoke.cmd_smoke(flags, pos))
4038
+
3997
4039
  elif cmd == "quickstart":
3998
4040
  import importlib
3999
4041
  _qs = importlib.import_module("meshcode.quickstart")