meshcode 2.11.154__tar.gz → 2.11.155__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.154 → meshcode-2.11.155}/PKG-INFO +1 -1
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/__init__.py +1 -1
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/run_agent.py +101 -11
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode.egg-info/SOURCES.txt +2 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/pyproject.toml +1 -1
- meshcode-2.11.155/tests/test_replica_base_workspace_fallback.py +87 -0
- meshcode-2.11.155/tests/test_replica_boot_protocol_unconditional.py +64 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/README.md +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/__main__.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/_launch_smoke.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/_update_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/cli.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/compat.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/daemon.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/doctor.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/helper_visuals.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/hooks/push_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/hostd.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/invites.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/launcher.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/swarm.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_swarm.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/preferences.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/secrets.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/self_update.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/up.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/upload.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/setup.cfg +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_core.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_doctor.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_helper_visuals.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_hostd_launch_pinned_env.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_hostd_serve_discovery_split.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_hostd_zombie_sessions.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_launch_smoke.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_live_mesh_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_preflight_hb_gate.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_pretrust_claude.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_push_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_rm_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_session_replay_gate.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_stop_ghost_terminal.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_swarm_events.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_task_progress.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_terminal_lifecycle.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_up_launch_cmd.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_update_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -933,11 +933,18 @@ def _preflight_hb_enabled() -> bool:
|
|
|
933
933
|
return os.environ.get("MESHCODE_HOSTD_SPAWN") != "1"
|
|
934
934
|
|
|
935
935
|
|
|
936
|
-
def _report_launch_failure(agent: str, project: str, reason: str, detail: str
|
|
936
|
+
def _report_launch_failure(agent: str, project: str, reason: str, detail: str,
|
|
937
|
+
status: str = "offline") -> None:
|
|
937
938
|
"""Editor spawn failed AFTER the pre-flight heartbeat already marked the
|
|
938
939
|
agent online — revert the ghost 'online' row to offline and tell the
|
|
939
940
|
dashboard WHY (launch-reliability fix 809d3b37, factors C + H).
|
|
940
941
|
|
|
942
|
+
`status` (must be a valid mc_agents.status per valid_agent_status): default
|
|
943
|
+
'offline' reverts a ghost-online editor-spawn failure. The replica
|
|
944
|
+
workspace-provisioning failure (task 9698d738) passes 'needs_setup' — the
|
|
945
|
+
failure IS an unprovisioned workspace, so the dashboard CTA matches the fix
|
|
946
|
+
(`meshcode setup <proj> <agent>`). 'error' is NOT a valid status.
|
|
947
|
+
|
|
941
948
|
This replaces a broken inline block in run()'s FileNotFoundError handler
|
|
942
949
|
that referenced `api_key`/`project_id` — names that are never bound in
|
|
943
950
|
run()'s scope — so the very RPC meant to surface "claude not installed"
|
|
@@ -987,12 +994,13 @@ def _report_launch_failure(agent: str, project: str, reason: str, detail: str) -
|
|
|
987
994
|
except Exception:
|
|
988
995
|
pass
|
|
989
996
|
|
|
990
|
-
# 2)
|
|
997
|
+
# 2) Set the failure status (default 'offline' reverts the pre-flight ghost;
|
|
998
|
+
# 'needs_setup' for an unprovisioned replica workspace — factor C).
|
|
991
999
|
status_body = json.dumps({
|
|
992
1000
|
"p_api_key": api_key,
|
|
993
1001
|
"p_project_id": project_id,
|
|
994
1002
|
"p_agent_name": agent,
|
|
995
|
-
"p_status":
|
|
1003
|
+
"p_status": status,
|
|
996
1004
|
"p_task": f"launch failed: {reason}",
|
|
997
1005
|
})
|
|
998
1006
|
status_req = Request(
|
|
@@ -1005,6 +1013,49 @@ def _report_launch_failure(agent: str, project: str, reason: str, detail: str) -
|
|
|
1005
1013
|
print(f"[meshcode] Launch-failure report skipped: {e}", file=sys.stderr)
|
|
1006
1014
|
|
|
1007
1015
|
|
|
1016
|
+
def _provision_replica_mcp_from_base(project: str, agent: str, ws: "Path") -> bool:
|
|
1017
|
+
"""BLINDAJE (task 9698d738 / Samuel P0 "réplicas prenden pero no bootean"):
|
|
1018
|
+
provision a replica's .mcp.json by cloning the BASE agent's known-good one when a
|
|
1019
|
+
fresh setup_workspace() fails (e.g. the keychain is unreadable non-interactively at
|
|
1020
|
+
hostd spawn -> setup_workspace returns 2 -> run_agent used to `return 2` and DIE
|
|
1021
|
+
before the MCP server even started: instance_id NULL, heartbeat NEVER, never ran the
|
|
1022
|
+
lease = "prende pero no bootea").
|
|
1023
|
+
|
|
1024
|
+
A replica named '<base>-<N>' is the same user's clone, so the base's MCP server
|
|
1025
|
+
block (api key / project_id / keychain profile baked at its last setup) is valid for
|
|
1026
|
+
it. We copy it, swap ONLY the server id + MESHCODE_AGENT, and write it to the replica
|
|
1027
|
+
workspace. The base workspace exists whenever the base agent booted (the common
|
|
1028
|
+
case — and the empirically healthy replicas all had a pre-existing workspace).
|
|
1029
|
+
Returns True iff a .mcp.json was written. Never raises."""
|
|
1030
|
+
try:
|
|
1031
|
+
import re as _re
|
|
1032
|
+
m = _re.match(r"^(?P<base>.+)-(?P<n>\d+)$", agent)
|
|
1033
|
+
if not m:
|
|
1034
|
+
return False # not a '<base>-<N>' replica name — nothing to clone
|
|
1035
|
+
base = m.group("base")
|
|
1036
|
+
base_mcp = WORKSPACES_ROOT / f"{project}-{base}" / ".mcp.json"
|
|
1037
|
+
if not base_mcp.exists():
|
|
1038
|
+
return False
|
|
1039
|
+
doc = json.loads(base_mcp.read_text(encoding="utf-8"))
|
|
1040
|
+
servers = doc.get("mcpServers") or {}
|
|
1041
|
+
if not servers:
|
|
1042
|
+
return False
|
|
1043
|
+
# base workspaces hold exactly one server block
|
|
1044
|
+
_base_id, block = next(iter(servers.items()))
|
|
1045
|
+
block = json.loads(json.dumps(block)) # deep copy so we don't mutate the source
|
|
1046
|
+
env = block.get("env")
|
|
1047
|
+
if isinstance(env, dict):
|
|
1048
|
+
env["MESHCODE_AGENT"] = agent # the only identity that differs from the base
|
|
1049
|
+
new_id = f"meshcode-{project}-{agent}"
|
|
1050
|
+
ws.mkdir(parents=True, exist_ok=True)
|
|
1051
|
+
(ws / ".mcp.json").write_text(
|
|
1052
|
+
json.dumps({"mcpServers": {new_id: block}}, indent=2) + "\n", encoding="utf-8")
|
|
1053
|
+
return (ws / ".mcp.json").exists()
|
|
1054
|
+
except Exception as e:
|
|
1055
|
+
print(f"[meshcode] replica base-fallback .mcp.json failed: {e}", file=sys.stderr)
|
|
1056
|
+
return False
|
|
1057
|
+
|
|
1058
|
+
|
|
1008
1059
|
# Repo-scoped launch (task 24e3dd44 / core-commander launch-diff). When `meshcode run
|
|
1009
1060
|
# <agent> --repo <path>` is used, the agent boots with cwd=repo (not the meshcode
|
|
1010
1061
|
# workspace), so its repo CLAUDE.md loads — we carry the boot protocol via
|
|
@@ -1293,19 +1344,44 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
1293
1344
|
if not mcp_json_path.exists():
|
|
1294
1345
|
# Regenerate via setup_workspace (has all data it needs from server)
|
|
1295
1346
|
print(f"[meshcode] .mcp.json missing from workspace — regenerating...", file=sys.stderr)
|
|
1347
|
+
_regen_ok = False
|
|
1348
|
+
_regen_err = ""
|
|
1296
1349
|
try:
|
|
1297
1350
|
from .setup_clients import setup_workspace
|
|
1298
1351
|
rc = setup_workspace(resolved_project, agent,
|
|
1299
1352
|
keychain_profile=_preferred_keychain_profile(resolved_project, agent))
|
|
1300
|
-
|
|
1353
|
+
_regen_ok = (rc == 0 and mcp_json_path.exists())
|
|
1354
|
+
if _regen_ok:
|
|
1301
1355
|
print(f"[meshcode] .mcp.json regenerated successfully.", file=sys.stderr)
|
|
1302
1356
|
else:
|
|
1303
|
-
|
|
1304
|
-
print(f"[meshcode]
|
|
1305
|
-
return 2
|
|
1357
|
+
_regen_err = f"setup_workspace rc={rc}"
|
|
1358
|
+
print(f"[meshcode] ERROR: could not regenerate .mcp.json ({_regen_err}).", file=sys.stderr)
|
|
1306
1359
|
except Exception as e:
|
|
1360
|
+
_regen_err = repr(e)
|
|
1307
1361
|
print(f"[meshcode] ERROR: .mcp.json missing and regeneration failed: {e}", file=sys.stderr)
|
|
1308
|
-
|
|
1362
|
+
|
|
1363
|
+
# BLINDAJE 1 (task 9698d738): replica base-workspace fallback. A replica whose
|
|
1364
|
+
# workspace was never provisioned hits this path; if setup_workspace fails (e.g.
|
|
1365
|
+
# keychain unreadable non-interactively at spawn) the process used to `return 2`
|
|
1366
|
+
# and DIE pre-boot ("prende pero no bootea"). Clone the base agent's known-good
|
|
1367
|
+
# .mcp.json so the replica boots anyway.
|
|
1368
|
+
if not _regen_ok and _provision_replica_mcp_from_base(resolved_project, agent, ws) \
|
|
1369
|
+
and mcp_json_path.exists():
|
|
1370
|
+
print(f"[meshcode] .mcp.json provisioned from base agent (replica fallback, "
|
|
1371
|
+
f"task 9698d738).", file=sys.stderr)
|
|
1372
|
+
_regen_ok = True
|
|
1373
|
+
|
|
1374
|
+
# BLINDAJE 2 (task 9698d738): never die SILENT. If we still can't provision,
|
|
1375
|
+
# report the launch as FAILED (mc_resolve_launch status='failed' + status row)
|
|
1376
|
+
# so the dashboard shows WHY instead of a phantom 'spawned but offline' replica.
|
|
1377
|
+
if not _regen_ok:
|
|
1378
|
+
_report_launch_failure(
|
|
1379
|
+
agent, resolved_project,
|
|
1380
|
+
"workspace .mcp.json could not be provisioned",
|
|
1381
|
+
f"setup_workspace failed ({_regen_err or 'unknown'}) and no base "
|
|
1382
|
+
f"workspace to clone — run `meshcode setup {resolved_project} {agent}`",
|
|
1383
|
+
status="needs_setup")
|
|
1384
|
+
print(f"[meshcode] Run `meshcode setup {resolved_project} {agent}` manually.", file=sys.stderr)
|
|
1309
1385
|
return 2
|
|
1310
1386
|
|
|
1311
1387
|
# R2-5 (2.11.117) boot-always-latest: resolve target version (explicit pin
|
|
@@ -1672,10 +1748,24 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
1672
1748
|
cmd += [
|
|
1673
1749
|
"--mcp-config", str(mcp_json_path),
|
|
1674
1750
|
"--settings", str(ws / ".claude" / "settings.json"),
|
|
1675
|
-
"--append-system-prompt", MESHCODE_BOOT_PROTOCOL.format(
|
|
1676
|
-
agent=agent, project=resolved_project,
|
|
1677
|
-
role=(agent_role or "MCP-connected agent"), repo=repo_lock),
|
|
1678
1751
|
]
|
|
1752
|
+
# Boot protocol — ALWAYS injected (task 9698d738 / Samuel P0 "réplicas prenden
|
|
1753
|
+
# pero NO bootean en TODAS las meshes"). It was gated on repo_lock on the
|
|
1754
|
+
# assumption that non-repo launches load the protocol from the workspace
|
|
1755
|
+
# CLAUDE.md at cwd=ws. But hostd-spawned REPLICAS frequently have NO provisioned
|
|
1756
|
+
# ws/CLAUDE.md (mc_replicate_agent creates the DB row, not the disk workspace)
|
|
1757
|
+
# and/or chdir lands elsewhere -> with --append gated OFF they received ZERO boot
|
|
1758
|
+
# instructions -> claude starts (spawn) but never runs SESSION START -> never
|
|
1759
|
+
# boots, in every mesh. Injecting unconditionally GUARANTEES boot regardless of
|
|
1760
|
+
# workspace provisioning / cwd, and is idempotent with a present CLAUDE.md (same
|
|
1761
|
+
# text), so repo-scoped and provisioned launches are unchanged. repo defaults to
|
|
1762
|
+
# the workspace path when there's no repo-lock.
|
|
1763
|
+
cmd += [
|
|
1764
|
+
"--append-system-prompt", MESHCODE_BOOT_PROTOCOL.format(
|
|
1765
|
+
agent=agent, project=resolved_project,
|
|
1766
|
+
role=(agent_role or "MCP-connected agent"),
|
|
1767
|
+
repo=(repo_lock or str(ws))),
|
|
1768
|
+
]
|
|
1679
1769
|
cmd.extend(["--", "boot"])
|
|
1680
1770
|
_launch_cwd = repo_lock or str(ws)
|
|
1681
1771
|
if not os.path.isdir(_launch_cwd):
|
|
@@ -91,6 +91,8 @@ tests/test_preflight_hb_gate.py
|
|
|
91
91
|
tests/test_pretrust_claude.py
|
|
92
92
|
tests/test_push_guard.py
|
|
93
93
|
tests/test_realtime_event_freshness.py
|
|
94
|
+
tests/test_replica_base_workspace_fallback.py
|
|
95
|
+
tests/test_replica_boot_protocol_unconditional.py
|
|
94
96
|
tests/test_rls_cross_tenant.py
|
|
95
97
|
tests/test_rm_guard.py
|
|
96
98
|
tests/test_rpc_grants.py
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Regression test for task 9698d738 (Samuel P0): "réplicas prenden pero NO bootean".
|
|
2
|
+
|
|
3
|
+
REAL ROOT CAUSE (Firma A, empirically confirmed): a replica whose workspace was never
|
|
4
|
+
provisioned hits run_agent.py's ".mcp.json missing" path; setup_workspace() fails (e.g.
|
|
5
|
+
the keychain is unreadable non-interactively at hostd spawn) -> run_agent used to
|
|
6
|
+
`return 2` and the process DIED before the MCP server started (instance_id NULL,
|
|
7
|
+
heartbeat NEVER, never ran mc_acquire_agent_lease). Healthy replicas only differed by
|
|
8
|
+
having a PRE-EXISTING workspace (so they skipped setup_workspace).
|
|
9
|
+
|
|
10
|
+
FIX: _provision_replica_mcp_from_base() clones the BASE agent's known-good .mcp.json
|
|
11
|
+
for the replica (same user -> valid creds), so the replica boots even when fresh setup
|
|
12
|
+
fails. Plus a LOUD-fail (mc_resolve_launch status='failed') so a still-unprovisionable
|
|
13
|
+
replica is visible instead of a silent phantom (covered by the existing
|
|
14
|
+
_report_launch_failure reporter).
|
|
15
|
+
|
|
16
|
+
These tests exercise the fallback helper in isolation (run_agent's launch fn execs
|
|
17
|
+
claude + chdirs, so it isn't unit-callable end-to-end).
|
|
18
|
+
"""
|
|
19
|
+
import json
|
|
20
|
+
import importlib
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
ra = importlib.import_module("meshcode.run_agent")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _write_base_workspace(root, project, base):
|
|
28
|
+
ws = root / f"{project}-{base}"
|
|
29
|
+
ws.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
doc = {
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
f"meshcode-{project}-{base}": {
|
|
33
|
+
"command": "python",
|
|
34
|
+
"args": ["-m", "meshcode.meshcode_mcp", "serve"],
|
|
35
|
+
"env": {
|
|
36
|
+
"MESHCODE_AGENT": base,
|
|
37
|
+
"MESHCODE_PROJECT": project,
|
|
38
|
+
"MESHCODE_PROJECT_ID": "pid-123",
|
|
39
|
+
"MESHCODE_KEYCHAIN_PROFILE": "default",
|
|
40
|
+
"MESHCODE_API_KEY": "mc_basekey",
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
(ws / ".mcp.json").write_text(json.dumps(doc), encoding="utf-8")
|
|
46
|
+
return ws
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_replica_clones_base_mcp(tmp_path, monkeypatch):
|
|
50
|
+
monkeypatch.setattr(ra, "WORKSPACES_ROOT", tmp_path)
|
|
51
|
+
_write_base_workspace(tmp_path, "ca-growth", "commander")
|
|
52
|
+
|
|
53
|
+
replica_ws = tmp_path / "ca-growth-commander-1"
|
|
54
|
+
assert ra._provision_replica_mcp_from_base("ca-growth", "commander-1", replica_ws) is True
|
|
55
|
+
|
|
56
|
+
doc = json.loads((replica_ws / ".mcp.json").read_text(encoding="utf-8"))
|
|
57
|
+
servers = doc["mcpServers"]
|
|
58
|
+
# server id is rewritten to the replica's identity
|
|
59
|
+
assert "meshcode-ca-growth-commander-1" in servers
|
|
60
|
+
block = servers["meshcode-ca-growth-commander-1"]
|
|
61
|
+
# ONLY MESHCODE_AGENT changes — the rest is inherited from the base (valid creds)
|
|
62
|
+
assert block["env"]["MESHCODE_AGENT"] == "commander-1"
|
|
63
|
+
assert block["env"]["MESHCODE_API_KEY"] == "mc_basekey"
|
|
64
|
+
assert block["env"]["MESHCODE_PROJECT"] == "ca-growth"
|
|
65
|
+
assert block["env"]["MESHCODE_PROJECT_ID"] == "pid-123"
|
|
66
|
+
# command/args carried verbatim
|
|
67
|
+
assert block["args"] == ["-m", "meshcode.meshcode_mcp", "serve"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_non_replica_name_is_not_cloned(tmp_path, monkeypatch):
|
|
71
|
+
monkeypatch.setattr(ra, "WORKSPACES_ROOT", tmp_path)
|
|
72
|
+
_write_base_workspace(tmp_path, "ca-growth", "commander")
|
|
73
|
+
# 'commander' has no -<N> suffix -> not a replica -> no clone attempted.
|
|
74
|
+
assert ra._provision_replica_mcp_from_base(
|
|
75
|
+
"ca-growth", "commander", tmp_path / "ca-growth-commander") is False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_missing_base_workspace_returns_false(tmp_path, monkeypatch):
|
|
79
|
+
monkeypatch.setattr(ra, "WORKSPACES_ROOT", tmp_path)
|
|
80
|
+
# base workspace absent -> nothing to clone -> False (caller then LOUD-fails).
|
|
81
|
+
assert ra._provision_replica_mcp_from_base(
|
|
82
|
+
"ca-growth", "commander-2", tmp_path / "ca-growth-commander-2") is False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
import sys
|
|
87
|
+
sys.exit(pytest.main([__file__, "-q"]))
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Regression sentinel for task 9698d738 (Samuel P0): "réplicas prenden pero NO
|
|
2
|
+
bootean en TODAS las meshes".
|
|
3
|
+
|
|
4
|
+
ROOT CAUSE: run_agent.py injected the mesh boot protocol via
|
|
5
|
+
`--append-system-prompt MESHCODE_BOOT_PROTOCOL` ONLY inside `if repo_lock:`. But
|
|
6
|
+
hostd-spawned REPLICAS launch with repo_lock=None (no `--repo`) and frequently have
|
|
7
|
+
NO provisioned workspace CLAUDE.md (mc_replicate_agent creates the DB row, not the
|
|
8
|
+
disk workspace). With the --append gated off and no CLAUDE.md to fall back on, the
|
|
9
|
+
replica's Claude Code session received ZERO boot instructions -> it started (the
|
|
10
|
+
terminal "prende") but never ran SESSION START -> never called meshcode_boot ->
|
|
11
|
+
spawn-but-no-boot, in every mesh.
|
|
12
|
+
|
|
13
|
+
FIX: inject the boot protocol UNCONDITIONALLY (idempotent with a present CLAUDE.md).
|
|
14
|
+
|
|
15
|
+
This test fails if anyone re-nests the boot protocol under `if repo_lock:`.
|
|
16
|
+
Static AST guard (the launch fn execs claude + chdirs, so it isn't unit-callable).
|
|
17
|
+
"""
|
|
18
|
+
import ast
|
|
19
|
+
import pathlib
|
|
20
|
+
|
|
21
|
+
RUN_AGENT = pathlib.Path(__file__).parent.parent / "meshcode" / "run_agent.py"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _names_inside_repo_lock_true_branches(tree: ast.AST) -> set[str]:
|
|
25
|
+
"""Every Name id referenced inside the TRUE branch of any `if repo_lock:`."""
|
|
26
|
+
found: set[str] = set()
|
|
27
|
+
for node in ast.walk(tree):
|
|
28
|
+
if isinstance(node, ast.If) and isinstance(node.test, ast.Name) \
|
|
29
|
+
and node.test.id == "repo_lock":
|
|
30
|
+
for stmt in node.body: # True branch only
|
|
31
|
+
for sub in ast.walk(stmt):
|
|
32
|
+
if isinstance(sub, ast.Name):
|
|
33
|
+
found.add(sub.id)
|
|
34
|
+
return found
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_boot_protocol_injected_unconditionally():
|
|
38
|
+
tree = ast.parse(RUN_AGENT.read_text(encoding="utf-8"))
|
|
39
|
+
|
|
40
|
+
all_names = {n.id for n in ast.walk(tree) if isinstance(n, ast.Name)}
|
|
41
|
+
assert "MESHCODE_BOOT_PROTOCOL" in all_names, \
|
|
42
|
+
"MESHCODE_BOOT_PROTOCOL is no longer referenced in run_agent.py — boot protocol lost."
|
|
43
|
+
|
|
44
|
+
gated = _names_inside_repo_lock_true_branches(tree)
|
|
45
|
+
assert "MESHCODE_BOOT_PROTOCOL" not in gated, (
|
|
46
|
+
"REGRESSION (task 9698d738): the boot protocol (--append-system-prompt "
|
|
47
|
+
"MESHCODE_BOOT_PROTOCOL) is gated under `if repo_lock:` again. hostd-spawned "
|
|
48
|
+
"replicas have repo_lock=None and often no ws/CLAUDE.md -> they spawn but never "
|
|
49
|
+
"boot. Inject MESHCODE_BOOT_PROTOCOL unconditionally (outside the repo_lock if)."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_append_system_prompt_flag_present():
|
|
54
|
+
"""The --append-system-prompt flag must still be emitted (it's the carrier)."""
|
|
55
|
+
consts = {n.value for n in ast.walk(ast.parse(RUN_AGENT.read_text(encoding="utf-8")))
|
|
56
|
+
if isinstance(n, ast.Constant) and isinstance(n.value, str)}
|
|
57
|
+
assert "--append-system-prompt" in consts, \
|
|
58
|
+
"--append-system-prompt flag missing — the boot protocol is no longer delivered."
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
test_boot_protocol_injected_unconditionally()
|
|
63
|
+
test_append_system_prompt_flag_present()
|
|
64
|
+
print("OK")
|
|
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
|
|
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
|