meshcode 2.11.166__tar.gz → 2.11.168__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 (119) hide show
  1. {meshcode-2.11.166 → meshcode-2.11.168}/PKG-INFO +1 -1
  2. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/comms_v4.py +30 -0
  4. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/run_agent.py +67 -2
  5. meshcode-2.11.168/meshcode/terminal_mirror_runner.py +478 -0
  6. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode.egg-info/SOURCES.txt +1 -0
  8. {meshcode-2.11.166 → meshcode-2.11.168}/pyproject.toml +1 -1
  9. {meshcode-2.11.166 → meshcode-2.11.168}/README.md +0 -0
  10. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/__main__.py +0 -0
  11. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/_launch_smoke.py +0 -0
  12. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/_session_handoff_template.py +0 -0
  13. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/_stop_hook_template.py +0 -0
  14. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/_update_guard.py +0 -0
  15. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/ascii_art.py +0 -0
  16. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/atomic_push.py +0 -0
  17. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/claude_update.py +0 -0
  18. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/cli.py +0 -0
  19. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/compat.py +0 -0
  20. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/daemon.py +0 -0
  21. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/date_parse.py +0 -0
  22. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/doctor.py +0 -0
  23. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/error_hints.py +0 -0
  24. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/exceptions.py +0 -0
  25. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/hooks/__init__.py +0 -0
  26. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/hooks/push_guard.py +0 -0
  27. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/hooks/repo_path_lock.py +0 -0
  28. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/hostd.py +0 -0
  29. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/invites.py +0 -0
  30. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/launcher.py +0 -0
  31. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/launcher_install.py +0 -0
  32. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/meshcode_mcp/__init__.py +0 -0
  33. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/meshcode_mcp/__main__.py +0 -0
  34. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/meshcode_mcp/backend.py +0 -0
  35. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/meshcode_mcp/realtime.py +0 -0
  36. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/meshcode_mcp/server.py +0 -0
  37. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  38. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/meshcode_mcp/test_backend.py +0 -0
  39. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  40. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  41. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  42. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  43. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  44. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/preferences.py +0 -0
  45. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/protocol_handler.py +0 -0
  46. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/protocol_v2.py +0 -0
  47. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/quickstart.py +0 -0
  48. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/rpc_allowlist.py +0 -0
  49. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/scripts/check_secrets.py +0 -0
  50. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/scripts/race_rate_harness.py +0 -0
  51. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/secrets.py +0 -0
  52. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/self_update.py +0 -0
  53. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/setup_clients.py +0 -0
  54. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/supervisor.py +0 -0
  55. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/up.py +0 -0
  56. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode/upload.py +0 -0
  57. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode.egg-info/dependency_links.txt +0 -0
  58. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode.egg-info/entry_points.txt +0 -0
  59. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode.egg-info/requires.txt +0 -0
  60. {meshcode-2.11.166 → meshcode-2.11.168}/meshcode.egg-info/top_level.txt +0 -0
  61. {meshcode-2.11.166 → meshcode-2.11.168}/setup.cfg +0 -0
  62. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_auto_update_hardening.py +0 -0
  63. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_autonomous_closegap_1.py +0 -0
  64. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_autonomous_closegap_2.py +0 -0
  65. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_autonomous_closegap_3.py +0 -0
  66. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_autonomous_prompt_inject.py +0 -0
  67. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_boot_bug_regression.py +0 -0
  68. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_color_truecolor.py +0 -0
  69. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_core.py +0 -0
  70. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_cross_agent_messaging.py +0 -0
  71. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_date_parse.py +0 -0
  72. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_doctor.py +0 -0
  73. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_ensure_boot_env_urgent_wake.py +0 -0
  74. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_epistemic_v1_python_sdk.py +0 -0
  75. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  76. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_esc_deaf_state.py +0 -0
  77. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_exceptions.py +0 -0
  78. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_file_upload.py +0 -0
  79. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_fleet_reaper.py +0 -0
  80. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_hostd_launch_pinned_env.py +0 -0
  81. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_hostd_serve_discovery_split.py +0 -0
  82. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_hostd_zombie_sessions.py +0 -0
  83. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_init_device_code.py +0 -0
  84. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_install_guard.py +0 -0
  85. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_launch_smoke.py +0 -0
  86. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_lease_sigterm_release.py +0 -0
  87. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_live_mesh_guard.py +0 -0
  88. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_mark_read_batch.py +0 -0
  89. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_marketplace_ratings.py +0 -0
  90. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_migration_integrity.py +0 -0
  91. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_no_appleevents_on_sweep.py +0 -0
  92. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_preflight_hb_gate.py +0 -0
  93. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_pretrust_claude.py +0 -0
  94. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_prompt_dedup_budget.py +0 -0
  95. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_push_guard.py +0 -0
  96. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_realtime_event_freshness.py +0 -0
  97. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_replica_base_workspace_fallback.py +0 -0
  98. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_replica_boot_protocol_unconditional.py +0 -0
  99. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_rls_cross_tenant.py +0 -0
  100. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_rm_guard.py +0 -0
  101. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_rpc_grants.py +0 -0
  102. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_rpc_migrations.py +0 -0
  103. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_run_agent_dry_run.py +0 -0
  104. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_run_agent_no_server_import.py +0 -0
  105. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_security_regressions.py +0 -0
  106. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_self_update_user_site.py +0 -0
  107. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_sentinel.py +0 -0
  108. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_session_replay_gate.py +0 -0
  109. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_setup_path.py +0 -0
  110. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_sleep_signals.py +0 -0
  111. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_status_enum_coverage.py +0 -0
  112. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_stay_on_loop_hook.py +0 -0
  113. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_stop_ghost_terminal.py +0 -0
  114. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_task_progress.py +0 -0
  115. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_terminal_lifecycle.py +0 -0
  116. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_up_launch_cmd.py +0 -0
  117. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_update_guard.py +0 -0
  118. {meshcode-2.11.166 → meshcode-2.11.168}/tests/test_urgent_wake_tmux.py +0 -0
  119. {meshcode-2.11.166 → meshcode-2.11.168}/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.166
3
+ Version: 2.11.168
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.166"
2
+ __version__ = "2.11.168"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -3603,6 +3603,36 @@ if __name__ == "__main__":
3603
3603
  autonomous_env = (os.environ.get("MESHCODE_AUTONOMOUS", "").strip().lower()
3604
3604
  in ("1", "true", "yes", "on"))
3605
3605
  autonomous = bool(flags.get("autonomous")) or autonomous_env
3606
+ # SELF-BUMP-BEFORE-SPAWN (task 36fe573d ext, Samuel 2026-07-02): a human typing
3607
+ # `meshcode run` must land on the LATEST meshcode WITHOUT ever updating by hand —
3608
+ # "meshcode se debe actualizar solo, its the whole point". The CC-autoupdate +
3609
+ # settings.json model-pin-strip fixes live in newer meshcode; a user stuck on an
3610
+ # old CLI (e.g. 2.11.144) would never receive them (chicken-egg). hostd already
3611
+ # runs `meshcode self-upgrade` as a launch pre-step, but a DIRECT human
3612
+ # `meshcode run/go` went straight to spawn and skipped it — this closes that gap.
3613
+ #
3614
+ # Reuses the deferral-safe blocking updater (self_update.check_and_maybe_update_blocking,
3615
+ # force=False): version-check-first + idempotent ("already latest" -> no-op), NEVER
3616
+ # raises (offline/PyPI-fail -> stays on installed), DEFERS when a live agent serve
3617
+ # shares this env (never clobbers a running MCP — the class#2-clobber guard), and
3618
+ # honors the MESHCODE_NO_UPDATE=1 / --no-update opt-out. On a REAL upgrade the OLD
3619
+ # code is already imported in-process, so we RE-EXEC the same argv under the fresh
3620
+ # interpreter/site so THIS spawn runs the new launcher (versioned-env spawn-latest +
3621
+ # CC-autoupdate + pin-strip). The _MESHCODE_SELF_BUMPED sentinel makes the re-exec
3622
+ # one-shot (no loop; the post-exec run is already latest anyway). Skipped on --dry-run.
3623
+ if not dry_run and not os.environ.get("_MESHCODE_SELF_BUMPED"):
3624
+ try:
3625
+ import importlib as _il
3626
+ _su = _il.import_module("meshcode.self_update")
3627
+ _bumped = _su.check_and_maybe_update_blocking(verbose=True, force=False)
3628
+ if _bumped:
3629
+ print(f"[meshcode] re-exec under meshcode {_bumped} (self-bump) ...",
3630
+ file=sys.stderr)
3631
+ os.environ["_MESHCODE_SELF_BUMPED"] = str(_bumped)
3632
+ os.execv(sys.executable,
3633
+ [sys.executable, "-m", "meshcode"] + sys.argv[1:])
3634
+ except Exception as _sbe:
3635
+ print(f"[meshcode] self-bump skipped: {_sbe}", file=sys.stderr)
3606
3636
  import importlib
3607
3637
  _run = importlib.import_module("meshcode.run_agent").run
3608
3638
  sys.exit(_run(agent, project=proj_override, editor_override=editor_override, permission_override=perm_override, dry_run=dry_run, autonomous=autonomous, repo_path=repo_override))
@@ -932,6 +932,44 @@ def _preflight_hb_enabled() -> bool:
932
932
  return os.environ.get("MESHCODE_HOSTD_SPAWN") != "1"
933
933
 
934
934
 
935
+ def _terminal_mirror_enabled() -> bool:
936
+ """M1 (d1720f2a): whether to wrap the editor under the terminal-mirror PTY
937
+ runner so the agent's REAL terminal streams to the owner dashboard
938
+ (mc_terminal_frame_push, mig 674). POSIX-only; Windows has no pty.fork().
939
+ Instant fleet-wide kill-switch: MESHCODE_TERMINAL_MIRROR=0."""
940
+ if sys.platform == "win32":
941
+ return False
942
+ v = (os.environ.get("MESHCODE_TERMINAL_MIRROR", "1") or "").strip().lower()
943
+ return v not in ("0", "false", "no", "off")
944
+
945
+
946
+ def _maybe_exec_under_terminal_mirror(cmd, agent: str, project: str) -> None:
947
+ """If the terminal mirror is enabled, REPLACE this process with the PTY
948
+ runner that execs `cmd` and tees its terminal to the dashboard. Returns
949
+ normally (no-op) when disabled/unavailable so the caller's plain os.execvp
950
+ runs unchanged. FAIL-OPEN: the mirror must NEVER break or block a launch
951
+ (storm RC e4eda167) — any error here just returns and the editor launches
952
+ directly. The runner self-resolves api_key (keychain) + project_id
953
+ (mc_resolve_project); we only hand it the non-secret agent/project names."""
954
+ try:
955
+ if not _terminal_mirror_enabled():
956
+ return
957
+ runner = os.path.join(os.path.dirname(os.path.abspath(__file__)),
958
+ "terminal_mirror_runner.py")
959
+ if not os.path.exists(runner):
960
+ return
961
+ os.environ.setdefault("MESHCODE_AGENT", agent)
962
+ os.environ.setdefault("MESHCODE_PROJECT", project)
963
+ sys.stdout.flush()
964
+ sys.stderr.flush()
965
+ os.execvp(sys.executable, [sys.executable, runner, "--", *cmd])
966
+ except Exception as e: # noqa: BLE001 — fall through to the caller's execvp
967
+ print(f"[meshcode] terminal-mirror wrap skipped: {e}", file=sys.stderr)
968
+ return
969
+
970
+
971
+
972
+
935
973
  def _report_launch_failure(agent: str, project: str, reason: str, detail: str,
936
974
  status: str = "offline") -> None:
937
975
  """Editor spawn failed AFTER the pre-flight heartbeat already marked the
@@ -1680,7 +1718,28 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
1680
1718
  _settings_model = None
1681
1719
  _sp = ws / ".claude" / "settings.json"
1682
1720
  if _sp.exists():
1683
- _settings_model = (json.loads(_sp.read_text(encoding="utf-8")) or {}).get("model")
1721
+ _sj = json.loads(_sp.read_text(encoding="utf-8")) or {}
1722
+ _settings_model = _sj.get("model")
1723
+ # DASHBOARD-MODEL-WINS (task 36fe573d, Samuel 2026-07-02): older meshcode
1724
+ # versions auto-pinned "model": PLATFORM_DEFAULT_MODEL into every workspace
1725
+ # settings.json. That legacy pin short-circuits the mc_agent_model_pref RPC
1726
+ # below, so the dashboard model dropdown (Fable 5, etc.) NEVER takes effect.
1727
+ # Current _meshcode_settings_dict() no longer writes it, but existing
1728
+ # workspaces keep the stale key forever (_heal_settings_hooks only touches
1729
+ # hooks). Strip it here ONLY when it equals the platform default (i.e. the
1730
+ # auto-written value) so a human-chosen NON-default model is preserved.
1731
+ # Net effect: dashboard pref becomes authoritative; if there is no pref,
1732
+ # PLATFORM_DEFAULT_MODEL is re-applied a few lines below -> same model, no
1733
+ # regression. Idempotent: after the first launch the key is gone.
1734
+ if _settings_model == PLATFORM_DEFAULT_MODEL:
1735
+ _sj.pop("model", None)
1736
+ try:
1737
+ _sp.write_text(json.dumps(_sj, indent=2) + "\n", encoding="utf-8")
1738
+ except Exception:
1739
+ pass
1740
+ _settings_model = None
1741
+ print("[meshcode] Stripped legacy settings.json model pin "
1742
+ "-> dashboard model_pref is now authoritative", file=sys.stderr)
1684
1743
  if not _settings_model:
1685
1744
  _mcp_cfg = json.loads(mcp_json_path.read_text(encoding="utf-8"))
1686
1745
  _env = (next(iter((_mcp_cfg.get("mcpServers") or {}).values()), {}) or {}).get("env", {}) or {}
@@ -1906,7 +1965,13 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
1906
1965
  _rc = 0
1907
1966
  sys.exit(_rc)
1908
1967
  else:
1909
- # Unix: replace this process with the editor
1968
+ # Unix: replace this process with the editor.
1969
+ # M1 (d1720f2a): when the terminal mirror is enabled, first exec a
1970
+ # thin PTY wrapper that tees the editor's REAL terminal to the owner
1971
+ # dashboard (mc_terminal_frame_push, mig 674). FAIL-OPEN — if the
1972
+ # wrap is disabled or fails for any reason this returns and the
1973
+ # original execvp below runs unchanged (launch is never blocked).
1974
+ _maybe_exec_under_terminal_mirror(cmd, agent, resolved_project)
1910
1975
  os.execvp(cmd[0], cmd)
1911
1976
  except FileNotFoundError:
1912
1977
  print(f"[meshcode] ERROR: '{editor}' not found in PATH", file=sys.stderr)
@@ -0,0 +1,478 @@
1
+ #!/usr/bin/env python3
2
+ """terminal_mirror_runner.py — PRODUCT integration of the terminal mirror (task M1 d1720f2a).
3
+
4
+ Wraps the agent's editor (Claude Code TUI) under a PTY so EVERY hostd/manually
5
+ launched agent streams its REAL terminal to the owner's dashboard via the
6
+ existing SECDEF RPC `mc_terminal_frame_push` (mig 674). This is the missing
7
+ link: the PoC relay (terminal_mirror_relay.py, task eacea0d8) + the DB channel
8
+ both shipped, but nothing ever invoked the relay on the real launch path, so
9
+ 0 frames were pushed in 7 days and every user saw an empty "Live terminal".
10
+
11
+ WHAT IT DOES
12
+ - pty.fork(): the child execs the editor command and owns the slave PTY as
13
+ its controlling tty (a TUI like Claude Code renders correctly).
14
+ - The parent is a transparent relay: it tees the child's output to the real
15
+ terminal (so the human sees exactly what they see today) AND into a
16
+ coalesced ring buffer that is redacted then pushed as frames.
17
+ - LOCAL interactivity is preserved: the human's keystrokes (stdin) are
18
+ forwarded to the child, and window-resize (SIGWINCH) is propagated to the
19
+ PTY so the TUI reflows. This is NOT the M2 remote-write path — it only
20
+ keeps today's local typing working.
21
+
22
+ SECURITY (Samuel hard rule, commander msg 210f541a — same posture as the PoC)
23
+ - Redaction happens HERE, before any network egress (primary filter); the RPC
24
+ re-scrubs server-side via _mc_scrub_terminal (defense-in-depth, mig 674).
25
+ - FAIL-CLOSED redaction: if redaction raises, the frame is replaced with an
26
+ error marker — un-redacted bytes are NEVER egressed.
27
+ - Transport = owner-scoped: frames land in mc_terminal_frames (RLS owner-read)
28
+ and reach ONLY the project owner's JWT. Never anon/public, never cross-mesh.
29
+ - The agent api_key is the credential (verified inside the SECDEF RPC); it is
30
+ resolved locally from the keychain and string-redacted from every frame.
31
+ - READ-ONLY w.r.t. the dashboard: there is NO path here for a remote party to
32
+ inject input into the child. (That is M2 780b6b0f, owner-gated + audited,
33
+ shipped separately after commander security sign-off.)
34
+
35
+ RELIABILITY — FAIL-OPEN (critical, given the storm RC e4eda167)
36
+ - The mirror must NEVER break or slow an agent launch. On ANY setup failure
37
+ (no tty, missing identity, import error, pty failure) the runner falls back
38
+ to a transparent `os.execvp` of the editor — identical to today's behavior.
39
+ - Pushes are async on a bounded drop-oldest queue, so a slow/offline network
40
+ can never stall the TUI.
41
+ - Diagnostics go to a logfile (~/.meshcode/logs/), never to the tty (which
42
+ would both corrupt the TUI and get captured back into frames).
43
+
44
+ ENABLE / KILL-SWITCH
45
+ MESHCODE_TERMINAL_MIRROR=0 -> disable (instant fleet-wide kill, no re-release).
46
+ Default = on (POSIX only). Windows has no pty.fork(); it always passes through.
47
+
48
+ USAGE (invoked by run_agent.py; not meant to be called by hand)
49
+ MESHCODE_AGENT=<name> MESHCODE_PROJECT=<mesh> \\
50
+ python3 terminal_mirror_runner.py -- <editor> [args...]
51
+ """
52
+ from __future__ import annotations
53
+
54
+ import os
55
+ import sys
56
+ import re
57
+ import json
58
+ import time
59
+ import errno
60
+ import struct
61
+ import select
62
+ import signal
63
+ import threading
64
+ import queue as _queue
65
+
66
+ # --- tunables (env-overridable) ------------------------------------------------
67
+ FLUSH_MS = int(os.environ.get("MIRROR_FLUSH_MS", "1000")) # coalesce window (~1-2s per task)
68
+ MAX_FRAME_BYTES = int(os.environ.get("MIRROR_MAX_FRAME_BYTES", "16384")) # force-flush cap
69
+ READ_CHUNK = 65536
70
+ QUEUE_MAX = 64 # bounded; drop-oldest on overflow
71
+ INPUT_POLL_MS = int(os.environ.get("MIRROR_INPUT_POLL_MS", "500")) # M2 remote-stdin poll cadence
72
+
73
+
74
+ def _enabled() -> bool:
75
+ if sys.platform == "win32":
76
+ return False
77
+ v = os.environ.get("MESHCODE_TERMINAL_MIRROR", "1").strip().lower()
78
+ return v not in ("0", "false", "no", "off")
79
+
80
+
81
+ def _input_pull_enabled() -> bool:
82
+ """M2 remote-stdin WRITE path (task 780b6b0f). DEFAULT OFF — this is the
83
+ owner-types-into-the-agent's-terminal seam, which is effectively code-exec on
84
+ the agent box, so it stays dark until the commander's post-canary FE-exposure
85
+ GO. The DB owner-only ACL (mig 687) already gates WHO may enqueue input; this
86
+ flag gates whether THIS runner drains+applies it at all. Opt-IN only."""
87
+ if sys.platform == "win32":
88
+ return False
89
+ v = os.environ.get("MESHCODE_TERMINAL_INPUT", "0").strip().lower()
90
+ return v in ("1", "true", "yes", "on")
91
+
92
+
93
+ def _log(msg: str) -> None:
94
+ """Best-effort diagnostics to a file — NEVER to the tty (would corrupt the
95
+ TUI and get mirrored back into frames)."""
96
+ try:
97
+ d = os.path.expanduser("~/.meshcode/logs")
98
+ os.makedirs(d, exist_ok=True)
99
+ agent = os.environ.get("MESHCODE_AGENT", "agent")
100
+ with open(os.path.join(d, f"terminal_mirror_{agent}.log"), "a", encoding="utf-8") as fh:
101
+ fh.write(f"{time.strftime('%Y-%m-%dT%H:%M:%S')} {msg}\n")
102
+ except Exception:
103
+ pass
104
+
105
+
106
+ # --- redaction: EXACT mirror of the PoC chain (_mc_scrub_pii THEN _mc_scrub_terminal,
107
+ # migs 674/674b/674c). The runner is the PRIMARY pre-egress filter, so it must be at
108
+ # least as strong as the DB net. Same order as the SQL. -------------------------------
109
+ _PATTERNS = [
110
+ # ---- _mc_scrub_pii layer (runs first in the DB) ----
111
+ (re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"), "<id>"),
112
+ (re.compile(r"/Users/[^/\s]+/[^\s]*"), "<path>"),
113
+ (re.compile(r"/home/[^/\s]+/[^\s]*"), "<path>"),
114
+ (re.compile(r"(?i)bearer\s+\S+"), "bearer <token>"),
115
+ # ---- _mc_scrub_terminal layer ----
116
+ (re.compile(r"-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----"), "<redacted:pem>"),
117
+ (re.compile(r"ya29\.[0-9A-Za-z_-]{10,}"), "<redacted:google_oauth>"),
118
+ (re.compile(r"xox[baprs]-[0-9A-Za-z-]{10,}"), "<redacted:slack>"),
119
+ (re.compile(r"([a-zA-Z][a-zA-Z0-9+.-]*://)[^/\s:@]+:[^/\s@]+@"), r"\1<redacted>@"),
120
+ (re.compile(r"sk-[A-Za-z0-9_-]{20,}"), "<redacted:key>"),
121
+ (re.compile(r"eyJ[A-Za-z0-9._-]{20,}"), "<redacted:jwt>"),
122
+ (re.compile(r"sb_secret_[A-Za-z0-9_-]{10,}"), "<redacted:sb_secret>"),
123
+ (re.compile(r"sbp_[A-Za-z0-9]{20,}"), "<redacted:sbp_pat>"),
124
+ (re.compile(r"(ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{20,}"), "<redacted:github>"),
125
+ (re.compile(r"pypi-[A-Za-z0-9_-]{20,}"), "<redacted:pypi>"),
126
+ (re.compile(r"AKIA[0-9A-Z]{16}"), "<redacted:aws>"),
127
+ (re.compile(r"mc_[A-Za-z0-9]{20,}"), "<redacted:mc_key>"),
128
+ (re.compile(r"(?i)\b((?:MESHCODE|SUPABASE|AWS|GOOGLE|ANTHROPIC|OPENAI|RESEND)_[A-Z0-9_]+)(\s*[=:]\s*)\S+"),
129
+ r"\1\2<redacted>"),
130
+ (re.compile(r"(?i)\b([A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PAT|PASSWORD|PASSWD))(\s*[=:]\s*)\S+"),
131
+ r"\1\2<redacted>"),
132
+ (re.compile(r"(?i)(password|passwd|secret|api[_-]?key|token)(\s*[=:]\s*)\S+"), r"\1\2<redacted>"),
133
+ ]
134
+
135
+
136
+ def _make_redactor(api_key: str):
137
+ def redact(s: str) -> str:
138
+ for pat, repl in _PATTERNS:
139
+ s = pat.sub(repl, s)
140
+ if api_key and len(api_key) > 6:
141
+ s = s.replace(api_key, "<redacted:self_key>")
142
+ return s
143
+ return redact
144
+
145
+
146
+ # --- identity / supabase resolution (self-sufficient; fail-open) ---------------
147
+ def _resolve_context():
148
+ """Return (supabase_url, anon, api_key, project_id, agent) or None on any
149
+ failure (-> caller passes through to a plain exec)."""
150
+ agent = os.environ.get("MESHCODE_AGENT") or ""
151
+ project = os.environ.get("MESHCODE_PROJECT") or ""
152
+ if not agent or not project:
153
+ return None
154
+ try:
155
+ from meshcode.setup_clients import _load_supabase_env
156
+ import meshcode.secrets as _secrets
157
+ import urllib.request as _u
158
+ sb = _load_supabase_env()
159
+ url = sb["SUPABASE_URL"].rstrip("/")
160
+ anon = sb["SUPABASE_KEY"]
161
+ profile = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
162
+ api_key = os.environ.get("MESHCODE_API_KEY") or _secrets.get_api_key(profile=profile)
163
+ if not api_key:
164
+ return None
165
+ req = _u.Request(
166
+ f"{url}/rest/v1/rpc/mc_resolve_project",
167
+ data=json.dumps({"p_api_key": api_key, "p_project_name": project}).encode(),
168
+ method="POST",
169
+ headers={"apikey": anon, "Authorization": f"Bearer {anon}",
170
+ "Content-Type": "application/json", "Content-Profile": "meshcode"},
171
+ )
172
+ with _u.urlopen(req, timeout=6) as resp:
173
+ proj = json.loads(resp.read().decode() or "null") or {}
174
+ project_id = proj.get("project_id")
175
+ if not project_id:
176
+ return None
177
+ return (url, anon, api_key, project_id, agent)
178
+ except Exception as e: # noqa: BLE001
179
+ _log(f"context resolve failed (passthrough): {type(e).__name__}: {e}")
180
+ return None
181
+
182
+
183
+ # --- winsize plumbing ----------------------------------------------------------
184
+ def _get_winsize(fd: int):
185
+ try:
186
+ import fcntl, termios
187
+ data = fcntl.ioctl(fd, termios.TIOCGWINSZ, b"\x00" * 8)
188
+ rows, cols, xp, yp = struct.unpack("HHHH", data)
189
+ return rows, cols, xp, yp
190
+ except Exception:
191
+ return (24, 80, 0, 0)
192
+
193
+
194
+ def _set_winsize(master_fd: int, dims) -> None:
195
+ try:
196
+ import fcntl, termios
197
+ fcntl.ioctl(master_fd, termios.TIOCSWINSZ, struct.pack("HHHH", *dims))
198
+ except Exception:
199
+ pass
200
+
201
+
202
+ # --- async, bounded, drop-oldest frame sender ----------------------------------
203
+ def _start_sender(url, anon, api_key, project_id, agent, redactor):
204
+ q: "_queue.Queue" = _queue.Queue(maxsize=QUEUE_MAX)
205
+ import urllib.request as _u
206
+
207
+ def _push(seq: int, chunk: str):
208
+ try:
209
+ body = json.dumps({
210
+ "p_api_key": api_key, "p_project_id": project_id,
211
+ "p_agent": agent, "p_seq": seq, "p_chunk": chunk,
212
+ }).encode("utf-8")
213
+ req = _u.Request(
214
+ f"{url}/rest/v1/rpc/mc_terminal_frame_push", data=body, method="POST",
215
+ headers={"Content-Type": "application/json",
216
+ "apikey": anon, "Authorization": f"Bearer {anon}"},
217
+ )
218
+ with _u.urlopen(req, timeout=10) as r:
219
+ r.read()
220
+ except Exception as e: # noqa: BLE001
221
+ _log(f"push seq={seq} err: {type(e).__name__}: {e}")
222
+
223
+ def _worker():
224
+ while True:
225
+ item = q.get()
226
+ if item is None:
227
+ return
228
+ seq, chunk = item
229
+ _push(seq, chunk)
230
+
231
+ t = threading.Thread(target=_worker, name="mirror-sender", daemon=True)
232
+ t.start()
233
+
234
+ def enqueue(seq: int, raw: str):
235
+ try:
236
+ clean = redactor(raw)
237
+ except Exception as e: # noqa: BLE001 — FAIL-CLOSED: never egress raw bytes
238
+ clean = f"<redaction-error: {type(e).__name__}; frame dropped>"
239
+ try:
240
+ q.put_nowait((seq, clean))
241
+ except _queue.Full:
242
+ # drop-oldest: a mirror may shed intermediate frames; the TUI repaints fully.
243
+ try:
244
+ q.get_nowait()
245
+ q.put_nowait((seq, clean))
246
+ except Exception:
247
+ pass
248
+
249
+ def stop():
250
+ try:
251
+ q.put_nowait(None)
252
+ except Exception:
253
+ pass
254
+ t.join(timeout=2.0)
255
+
256
+ return enqueue, stop
257
+
258
+
259
+ # --- M2 remote-stdin puller (gated OFF by default; task 780b6b0f) --------------
260
+ def _start_input_puller(url, anon, api_key, project_id, agent, master_fd):
261
+ """Periodically drain the owner-authored stdin channel and apply it to the
262
+ child's PTY master fd (== type into the agent's real terminal).
263
+
264
+ SECURITY POSTURE (why this is safe to STAGE but stays OFF):
265
+ * GATE 1 (this runner): _input_pull_enabled() default OFF — no drain, no
266
+ write, unless an operator explicitly opts in. Caller already checked it.
267
+ * GATE 2 (DB, mig 687): mc_terminal_input_pull is api-key gated and a SCOPED
268
+ agent key can drain ONLY its own agent's queue; the writer side
269
+ (mc_terminal_input_push) is OWNER-ONLY (members excluded). So even with
270
+ this seam live, only the project owner can author bytes that land here.
271
+ * CONSUME-ONCE: the RPC DELETEs as it returns; bytes are applied LITERALLY
272
+ (NOT redacted — the owner may be pasting a real token into a prompt) and
273
+ never persisted by the runner.
274
+ * FAIL-OPEN: any pull/parse/write error is logged and swallowed; the seam
275
+ can never break or stall the TUI (runs on its own daemon thread; the tee
276
+ loop is untouched).
277
+ Returns a stop() callable.
278
+ """
279
+ import urllib.request as _u
280
+ stop_evt = threading.Event()
281
+
282
+ def _pull_once():
283
+ body = json.dumps({
284
+ "p_api_key": api_key, "p_project_id": project_id, "p_agent": agent,
285
+ }).encode("utf-8")
286
+ req = _u.Request(
287
+ f"{url}/rest/v1/rpc/mc_terminal_input_pull", data=body, method="POST",
288
+ headers={"Content-Type": "application/json",
289
+ "apikey": anon, "Authorization": f"Bearer {anon}"},
290
+ )
291
+ with _u.urlopen(req, timeout=10) as r:
292
+ res = json.loads(r.read().decode() or "null") or {}
293
+ if not res.get("ok"):
294
+ return
295
+ for item in (res.get("inputs") or []):
296
+ data = item.get("data")
297
+ if not data:
298
+ continue
299
+ try:
300
+ os.write(master_fd, data.encode("utf-8")) # LITERAL bytes -> child stdin
301
+ except OSError as e:
302
+ _log(f"input apply err: {type(e).__name__}: {e}")
303
+
304
+ def _worker():
305
+ interval = INPUT_POLL_MS / 1000.0
306
+ while not stop_evt.wait(interval):
307
+ try:
308
+ _pull_once()
309
+ except Exception as e: # noqa: BLE001 — FAIL-OPEN: never break the TUI
310
+ _log(f"input pull err (continuing): {type(e).__name__}: {e}")
311
+
312
+ t = threading.Thread(target=_worker, name="mirror-input-puller", daemon=True)
313
+ t.start()
314
+
315
+ def stop():
316
+ stop_evt.set()
317
+ t.join(timeout=2.0)
318
+
319
+ return stop
320
+
321
+
322
+ def _passthrough_exec(cmd):
323
+ """Transparent fallback — identical to run_agent.py's original Unix path."""
324
+ os.execvp(cmd[0], cmd)
325
+
326
+
327
+ def main(argv) -> int:
328
+ if "--" in argv:
329
+ cmd = argv[argv.index("--") + 1:]
330
+ else:
331
+ cmd = argv[1:]
332
+ if not cmd:
333
+ sys.stderr.write("[mirror] no command to run\n")
334
+ return 2
335
+
336
+ if not _enabled():
337
+ _passthrough_exec(cmd)
338
+ return 127 # unreachable if execvp succeeds
339
+
340
+ ctx = _resolve_context()
341
+ if ctx is None:
342
+ _passthrough_exec(cmd)
343
+ return 127
344
+ url, anon, api_key, project_id, agent = ctx
345
+
346
+ import pty
347
+ try:
348
+ pid, master = pty.fork()
349
+ except Exception as e: # noqa: BLE001 — pty unavailable -> never break the launch
350
+ _log(f"pty.fork failed (passthrough): {type(e).__name__}: {e}")
351
+ _passthrough_exec(cmd)
352
+ return 127
353
+
354
+ if pid == 0: # child: becomes the editor; owns the slave PTY as controlling tty
355
+ try:
356
+ os.execvp(cmd[0], cmd)
357
+ except Exception:
358
+ os._exit(127)
359
+
360
+ # ---- parent: transparent relay ----
361
+ redactor = _make_redactor(api_key)
362
+ enqueue, stop_sender = _start_sender(url, anon, api_key, project_id, agent, redactor)
363
+
364
+ # M2 remote-stdin seam — DARK by default. Only spun up when explicitly opted
365
+ # in (MESHCODE_TERMINAL_INPUT=1); otherwise no drain thread exists at all.
366
+ stop_input_puller = None
367
+ if _input_pull_enabled():
368
+ try:
369
+ stop_input_puller = _start_input_puller(url, anon, api_key, project_id, agent, master)
370
+ _log("M2 remote-stdin puller ENABLED")
371
+ except Exception as e: # noqa: BLE001 — never break launch on the optional seam
372
+ _log(f"input puller start failed (continuing read-only): {type(e).__name__}: {e}")
373
+ stop_input_puller = None
374
+
375
+ out_fd = 1
376
+ in_fd = 0
377
+ # propagate the real terminal size to the PTY so the TUI renders correctly
378
+ _set_winsize(master, _get_winsize(out_fd))
379
+
380
+ def _on_winch(signum, frame):
381
+ _set_winsize(master, _get_winsize(out_fd))
382
+ try:
383
+ signal.signal(signal.SIGWINCH, _on_winch)
384
+ except Exception:
385
+ pass
386
+
387
+ saved_attrs = None
388
+ try:
389
+ import termios, tty
390
+ if os.isatty(in_fd):
391
+ saved_attrs = termios.tcgetattr(in_fd)
392
+ tty.setraw(in_fd)
393
+ except Exception:
394
+ saved_attrs = None
395
+
396
+ # seq seeded from epoch-ms => monotonic per agent ACROSS restarts (next run's
397
+ # seed > all prior frames), satisfying the per-(project,agent) ordering contract.
398
+ seq = int(time.time() * 1000)
399
+ buf = bytearray()
400
+ last_flush = time.monotonic()
401
+
402
+ def flush():
403
+ nonlocal seq, buf, last_flush
404
+ if buf:
405
+ enqueue(seq, bytes(buf).decode("utf-8", "replace"))
406
+ seq += 1
407
+ buf = bytearray()
408
+ last_flush = time.monotonic()
409
+
410
+ watch = [master, in_fd] if os.isatty(in_fd) else [master]
411
+ try:
412
+ while True:
413
+ timeout = FLUSH_MS / 1000.0
414
+ try:
415
+ r, _, _ = select.select(watch, [], [], timeout)
416
+ except (select.error, OSError) as e:
417
+ if getattr(e, "errno", None) == errno.EINTR:
418
+ continue # interrupted by SIGWINCH — re-arm
419
+ raise
420
+ if master in r:
421
+ try:
422
+ data = os.read(master, READ_CHUNK)
423
+ except OSError:
424
+ data = b""
425
+ if not data:
426
+ break # child exited / PTY closed
427
+ buf += data
428
+ try:
429
+ os.write(out_fd, data) # tee: the human's terminal is unaffected
430
+ except OSError:
431
+ pass
432
+ if len(buf) >= MAX_FRAME_BYTES:
433
+ flush()
434
+ # LOCAL stdin forwarding — preserves today's interactivity (NOT the
435
+ # gated M2 remote-write path).
436
+ if in_fd in r:
437
+ try:
438
+ inp = os.read(in_fd, READ_CHUNK)
439
+ except OSError:
440
+ inp = b""
441
+ if inp:
442
+ try:
443
+ os.write(master, inp)
444
+ except OSError:
445
+ pass
446
+ if (time.monotonic() - last_flush) * 1000 >= FLUSH_MS:
447
+ flush()
448
+ flush()
449
+ finally:
450
+ if saved_attrs is not None:
451
+ try:
452
+ import termios
453
+ termios.tcsetattr(in_fd, termios.TCSADRAIN, saved_attrs)
454
+ except Exception:
455
+ pass
456
+ if stop_input_puller is not None:
457
+ try:
458
+ stop_input_puller()
459
+ except Exception:
460
+ pass
461
+ stop_sender()
462
+
463
+ # propagate the child's exit status as our own
464
+ status = 0
465
+ try:
466
+ _, status = os.waitpid(pid, 0)
467
+ except Exception:
468
+ pass
469
+ if os.WIFSIGNALED(status):
470
+ return 128 + os.WTERMSIG(status)
471
+ return os.WEXITSTATUS(status)
472
+
473
+
474
+ if __name__ == "__main__":
475
+ try:
476
+ sys.exit(main(sys.argv))
477
+ except KeyboardInterrupt:
478
+ sys.exit(130)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.166
3
+ Version: 2.11.168
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -32,6 +32,7 @@ meshcode/secrets.py
32
32
  meshcode/self_update.py
33
33
  meshcode/setup_clients.py
34
34
  meshcode/supervisor.py
35
+ meshcode/terminal_mirror_runner.py
35
36
  meshcode/up.py
36
37
  meshcode/upload.py
37
38
  meshcode.egg-info/PKG-INFO
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.11.166"
7
+ version = "2.11.168"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes
File without changes