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.
- {meshcode-2.11.152 → meshcode-2.11.154}/PKG-INFO +1 -1
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/__init__.py +1 -1
- meshcode-2.11.154/meshcode/_launch_smoke.py +227 -0
- meshcode-2.11.154/meshcode/_update_guard.py +175 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/comms_v4.py +53 -11
- meshcode-2.11.154/meshcode/hooks/push_guard.py +208 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/hooks/repo_path_lock.py +52 -2
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/hostd.py +43 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/server.py +122 -24
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/protocol_handler.py +60 -27
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/run_agent.py +222 -26
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/self_update.py +12 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/up.py +32 -6
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode.egg-info/SOURCES.txt +9 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/pyproject.toml +1 -1
- meshcode-2.11.154/tests/test_launch_smoke.py +87 -0
- meshcode-2.11.154/tests/test_preflight_hb_gate.py +42 -0
- meshcode-2.11.154/tests/test_push_guard.py +123 -0
- meshcode-2.11.154/tests/test_rm_guard.py +56 -0
- meshcode-2.11.154/tests/test_up_launch_cmd.py +31 -0
- meshcode-2.11.154/tests/test_update_guard.py +96 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/README.md +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/__main__.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/cli.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/compat.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/daemon.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/doctor.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/helper_visuals.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/invites.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/launcher.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/swarm.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/meshcode_mcp/test_swarm.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/preferences.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/secrets.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode/upload.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/setup.cfg +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_core.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_doctor.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_helper_visuals.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_hostd_launch_pinned_env.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_hostd_serve_discovery_split.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_hostd_zombie_sessions.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_live_mesh_guard.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_pretrust_claude.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_session_replay_gate.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_stop_ghost_terminal.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_swarm_events.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_task_progress.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_terminal_lifecycle.py +0 -0
- {meshcode-2.11.152 → meshcode-2.11.154}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -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
|
-
|
|
3331
|
-
if not
|
|
3332
|
-
print(
|
|
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
|
-
|
|
3335
|
-
|
|
3336
|
-
if
|
|
3337
|
-
|
|
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} → {
|
|
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
|
-
|
|
3427
|
-
|
|
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")
|