meshcode 2.11.155__tar.gz → 2.11.158__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.155 → meshcode-2.11.158}/PKG-INFO +1 -1
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/__init__.py +1 -1
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/hostd.py +38 -48
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/server.py +52 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/protocol_handler.py +83 -4
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/self_update.py +10 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/setup_clients.py +8 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode.egg-info/SOURCES.txt +4 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/pyproject.toml +1 -1
- meshcode-2.11.158/tests/test_ensure_boot_env_urgent_wake.py +95 -0
- meshcode-2.11.158/tests/test_fleet_reaper.py +176 -0
- meshcode-2.11.158/tests/test_no_appleevents_on_sweep.py +81 -0
- meshcode-2.11.158/tests/test_urgent_wake_tmux.py +137 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/README.md +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/__main__.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/_launch_smoke.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/_update_guard.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/cli.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/compat.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/daemon.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/doctor.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/helper_visuals.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/hooks/push_guard.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/invites.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/launcher.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/swarm.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_swarm.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/preferences.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/run_agent.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/secrets.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/up.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/upload.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/setup.cfg +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_core.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_doctor.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_helper_visuals.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_hostd_launch_pinned_env.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_hostd_serve_discovery_split.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_hostd_zombie_sessions.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_launch_smoke.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_live_mesh_guard.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_preflight_hb_gate.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_pretrust_claude.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_push_guard.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_replica_base_workspace_fallback.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_replica_boot_protocol_unconditional.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_rm_guard.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_session_replay_gate.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_stop_ghost_terminal.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_swarm_events.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_task_progress.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_terminal_lifecycle.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_up_launch_cmd.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_update_guard.py +0 -0
- {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -1497,57 +1497,47 @@ def _terminal_window_marker(target: str) -> str:
|
|
|
1497
1497
|
|
|
1498
1498
|
|
|
1499
1499
|
def _list_dead_terminal_windows(target: str) -> list:
|
|
1500
|
-
"""
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
' end if\n'
|
|
1520
|
-
' end try\n'
|
|
1521
|
-
' end repeat\n'
|
|
1522
|
-
'end tell\n'
|
|
1523
|
-
'return out')
|
|
1524
|
-
try:
|
|
1525
|
-
out = subprocess.run(["/usr/bin/osascript", "-e", script],
|
|
1526
|
-
capture_output=True, text=True, timeout=10).stdout or ""
|
|
1527
|
-
return [int(x) for x in out.split() if x.strip().isdigit()]
|
|
1528
|
-
except Exception:
|
|
1529
|
-
return []
|
|
1500
|
+
"""DISABLED no-op (macOS TCC fix, task e0465408 / Prong 1 #2).
|
|
1501
|
+
|
|
1502
|
+
This used to enumerate Terminal windows via `osascript -e 'tell application
|
|
1503
|
+
"Terminal" ...'` to find '[Process completed]' rotation orphans (task
|
|
1504
|
+
70ac6d25 / 2.11.157). That Apple Event fired from python3.11 on the hostd
|
|
1505
|
+
lifecycle sweep and was RC-1 of the recurring macOS consent prompt
|
|
1506
|
+
"python3.11 would like to access data from other apps"
|
|
1507
|
+
(kTCCServiceAppleEvents). Because the interpreter is ad-hoc signed (bare
|
|
1508
|
+
cdhash, no Designated Requirement) the grant never stuck across versions, so
|
|
1509
|
+
it re-prompted on every sweep. The `.command` launcher already self-closes
|
|
1510
|
+
its OWN tab on any exit (clean / recycle / crash) via a tty-targeted close
|
|
1511
|
+
run from the agent's shell (see protocol_handler._write_fleet_native_agent),
|
|
1512
|
+
so this hostd-side reaper was a redundant belt. Removing it takes Apple
|
|
1513
|
+
Events off the python sweep path with no dead-tab regression — only
|
|
1514
|
+
un-trappable SIGKILL orphans may linger, which are rare and cannot be closed
|
|
1515
|
+
without Apple Events anyway. Kept as a no-op so the call sites stay harmless.
|
|
1516
|
+
NEVER reintroduce osascript here — guarded by
|
|
1517
|
+
tests/test_no_appleevents_on_sweep.py."""
|
|
1518
|
+
return []
|
|
1530
1519
|
|
|
1531
1520
|
|
|
1532
1521
|
def _close_dead_terminal_windows(target: str, window_ids) -> int:
|
|
1533
|
-
"""
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1522
|
+
"""DISABLED no-op (macOS TCC fix, task e0465408 / Prong 1 #2).
|
|
1523
|
+
|
|
1524
|
+
Previously closed dead launcher windows by firing
|
|
1525
|
+
`osascript -e 'tell application "Terminal" to close ...'` from python3.11.
|
|
1526
|
+
That Apple Event was RC-1 of the recurring TCC prompt
|
|
1527
|
+
("python3.11 would like to access data from other apps") — the adhoc python
|
|
1528
|
+
interpreter has no stable Designated Requirement, so the kTCCServiceAppleEvents
|
|
1529
|
+
grant never persists across per-version cdhash changes and re-prompts forever.
|
|
1530
|
+
|
|
1531
|
+
The `.command` launcher already self-closes its OWN tab on any exit
|
|
1532
|
+
(clean/recycle/crash) via a tty-targeted close run from the agent's SHELL
|
|
1533
|
+
(not python — different TCC attribution), so this hostd-side reaper was a
|
|
1534
|
+
redundant belt. Removing it takes Apple Events off the python sweep path with
|
|
1535
|
+
no dead-tab regression — only un-trappable SIGKILL orphans may linger, which
|
|
1536
|
+
are rare and cannot be closed without Apple Events anyway. Kept as a no-op so
|
|
1537
|
+
the call sites stay harmless.
|
|
1538
|
+
NEVER reintroduce osascript here — guarded by
|
|
1539
|
+
tests/test_no_appleevents_on_sweep.py."""
|
|
1540
|
+
return 0
|
|
1551
1541
|
|
|
1552
1542
|
|
|
1553
1543
|
def _spawn_rate_ok(target: str):
|
|
@@ -491,9 +491,61 @@ def _try_auto_wake(from_agent: str, preview: str, urgent: bool = False) -> None:
|
|
|
491
491
|
# configurations, pipelines, tmux without termguicolors) render the raw
|
|
492
492
|
# bytes — "[91m[1m[mesh][0m …" — which is worse UX than no color at all.
|
|
493
493
|
nudge = f"[mesh] {safe_agent} > {safe_preview} — meshcode_check()"
|
|
494
|
+
# tmux fast-path (task 780d7397): the fleet runs inside a tmux session, so the
|
|
495
|
+
# MCP serve process (a child of `claude` in the pane) inherits TMUX_PANE. When
|
|
496
|
+
# it's present, prefer `tmux send-keys` over the window-activation fallbacks —
|
|
497
|
+
# only tmux can ACTUALLY interrupt Claude's current turn (Escape) and then
|
|
498
|
+
# inject+submit the nudge, so an urgent message reaches a mid-step agent in
|
|
499
|
+
# ~1-2s instead of waiting for its next tool-call boundary. AppleScript/
|
|
500
|
+
# PowerShell/xdotool below cannot interrupt generation; they only help a truly
|
|
501
|
+
# idle agent and so remain the fallback when we're not inside tmux.
|
|
502
|
+
tmux_pane = os.environ.get("TMUX_PANE", "")
|
|
503
|
+
if tmux_pane and re.match(r'^%[0-9]+$', tmux_pane):
|
|
504
|
+
# Urgent gets a short, unambiguous marker that forces a meshcode_check —
|
|
505
|
+
# never the full payload (the agent reads the real message via the check).
|
|
506
|
+
marker = (f"[URGENT] {safe_agent} - run meshcode_check()"
|
|
507
|
+
if urgent else nudge)
|
|
508
|
+
try:
|
|
509
|
+
# 1) Escape interrupts Claude's in-flight generation/turn.
|
|
510
|
+
subprocess.run(["tmux", "send-keys", "-t", tmux_pane, "Escape"],
|
|
511
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
512
|
+
timeout=5)
|
|
513
|
+
# 2) brief pause so the interrupt registers before we type.
|
|
514
|
+
_time.sleep(0.2)
|
|
515
|
+
# 3) inject the marker literally (-l so it is never parsed as a key
|
|
516
|
+
# name), then submit with a separate Enter so Claude reads it now.
|
|
517
|
+
subprocess.run(["tmux", "send-keys", "-t", tmux_pane, "-l", marker],
|
|
518
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
519
|
+
timeout=5)
|
|
520
|
+
_time.sleep(0.15) # let the TUI register the typed text before submit
|
|
521
|
+
subprocess.run(["tmux", "send-keys", "-t", tmux_pane, "Enter"],
|
|
522
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
523
|
+
timeout=5)
|
|
524
|
+
log.info(f"auto-wake: interrupted+injected via tmux pane {tmux_pane}")
|
|
525
|
+
return
|
|
526
|
+
except Exception as e:
|
|
527
|
+
log.debug(f"auto-wake tmux path failed, falling back: {e}")
|
|
494
528
|
system = platform.system()
|
|
495
529
|
try:
|
|
496
530
|
if system == "Darwin":
|
|
531
|
+
# macOS TCC fix (task e0465408 / Prong 1 #3): the AppleScript
|
|
532
|
+
# keystroke fallback fires osascript from python3.11, which triggers
|
|
533
|
+
# the recurring "python3.11 would like to access data from other apps"
|
|
534
|
+
# consent prompt (kTCCServiceAppleEvents; adhoc python has no stable
|
|
535
|
+
# Designated Requirement so the grant never persists). tmux send-keys
|
|
536
|
+
# (the fast-path above) is the durable, prompt-free wake mechanism.
|
|
537
|
+
# The AppleScript fallback is OFF by default and only re-enabled by
|
|
538
|
+
# operators who explicitly opt into Apple Events:
|
|
539
|
+
# MESHCODE_AUTOWAKE_APPLESCRIPT=1
|
|
540
|
+
# When off and tmux was unavailable/failed, we skip keystroke
|
|
541
|
+
# injection entirely (no prompt, no wake) rather than re-prompt Samuel.
|
|
542
|
+
if os.environ.get("MESHCODE_AUTOWAKE_APPLESCRIPT", "").strip().lower() \
|
|
543
|
+
not in ("1", "true", "yes", "on"):
|
|
544
|
+
log.debug(
|
|
545
|
+
"auto-wake: tmux unavailable and AppleScript fallback "
|
|
546
|
+
"disabled (MESHCODE_AUTOWAKE_APPLESCRIPT off) — skipping "
|
|
547
|
+
"keystroke injection to avoid macOS TCC prompt")
|
|
548
|
+
return
|
|
497
549
|
# Sanitize app_name — TERM_PROGRAM is an env var, not DB-sourced,
|
|
498
550
|
# but defense-in-depth: only allow known terminal app names.
|
|
499
551
|
parent_app = os.environ.get("TERM_PROGRAM", "Terminal")
|
|
@@ -356,8 +356,12 @@ def _fleet_attach_macos(tmux: str) -> tuple[bool, str]:
|
|
|
356
356
|
]
|
|
357
357
|
try:
|
|
358
358
|
launch_dir.mkdir(parents=True, exist_ok=True)
|
|
359
|
+
# macOS TCC fix (task e0465408) security guardrail: launcher dir +
|
|
360
|
+
# .command owner-only (0o700). These scripts are LaunchServices-executed
|
|
361
|
+
# via `open -a` and must never be world-readable/-writable.
|
|
362
|
+
_harden_launcher_dir(launch_dir)
|
|
359
363
|
script_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
360
|
-
os.chmod(script_path,
|
|
364
|
+
os.chmod(script_path, 0o700)
|
|
361
365
|
except Exception as e:
|
|
362
366
|
return False, f"could not write fleet launcher: {e}"
|
|
363
367
|
r = subprocess.run(["open", "-a", "Terminal", str(script_path)],
|
|
@@ -373,6 +377,60 @@ def _fleet_attach_linux(tmux: str) -> tuple[bool, str]:
|
|
|
373
377
|
return _spawn_terminal_linux(attach)
|
|
374
378
|
|
|
375
379
|
|
|
380
|
+
def _reap_dead_fleet_windows(tmux: str, cap: Optional[int] = None) -> int:
|
|
381
|
+
"""Reap accumulated DEAD windows from the fleet tmux session (task 22fcdb21,
|
|
382
|
+
Samuel P1: dead panes stacked to 51).
|
|
383
|
+
|
|
384
|
+
The RAII trap in _fleet_wrap closes a window on a CLEAN / SIGTERM exit, but
|
|
385
|
+
a SIGKILL (rc=137: force-kill / OOM / recycle) cannot be trapped, so the
|
|
386
|
+
per-window `remain-on-exit=failed` keeps that dead pane forever. Over days
|
|
387
|
+
these pile up and `#{session_windows}` (the 'N agents' counter) counts
|
|
388
|
+
cadavers, not live agents.
|
|
389
|
+
|
|
390
|
+
Sweeps every fleet window whose panes are ALL dead, KEEPING only the `cap`
|
|
391
|
+
most-recently-dead (bounded crash-retention for debugging) and killing the
|
|
392
|
+
rest. It NEVER touches a window with a live pane, so it can never close a
|
|
393
|
+
running agent. Returns the count reaped. Best-effort; never raises. Tunable
|
|
394
|
+
via MESHCODE_FLEET_DEAD_CAP (default 3; 0 = reap every dead pane)."""
|
|
395
|
+
if cap is None:
|
|
396
|
+
try:
|
|
397
|
+
cap = int(os.environ.get("MESHCODE_FLEET_DEAD_CAP", "3") or 3)
|
|
398
|
+
except ValueError:
|
|
399
|
+
cap = 3
|
|
400
|
+
cap = max(0, cap)
|
|
401
|
+
try:
|
|
402
|
+
r = _tmux(tmux, "list-panes", "-s", "-t", f"={_FLEET_SESSION}",
|
|
403
|
+
"-F", "#{window_id}\t#{pane_dead}\t#{pane_dead_time}")
|
|
404
|
+
if r.returncode != 0:
|
|
405
|
+
return 0
|
|
406
|
+
# Group panes by window: a window is dead only if EVERY pane is dead
|
|
407
|
+
# (fleet windows are single-pane, but stay correct if that changes).
|
|
408
|
+
wins: dict = {}
|
|
409
|
+
for line in (r.stdout or "").splitlines():
|
|
410
|
+
parts = line.split("\t")
|
|
411
|
+
if len(parts) < 2:
|
|
412
|
+
continue
|
|
413
|
+
wid, pdead = parts[0], parts[1]
|
|
414
|
+
try:
|
|
415
|
+
pdt = int(parts[2]) if len(parts) > 2 and parts[2] else 0
|
|
416
|
+
except ValueError:
|
|
417
|
+
pdt = 0
|
|
418
|
+
w = wins.setdefault(wid, {"all_dead": True, "dt": 0})
|
|
419
|
+
if pdead != "1":
|
|
420
|
+
w["all_dead"] = False
|
|
421
|
+
else:
|
|
422
|
+
w["dt"] = max(w["dt"], pdt)
|
|
423
|
+
dead = sorted(((v["dt"], wid) for wid, v in wins.items() if v["all_dead"]),
|
|
424
|
+
reverse=True) # newest dead first; keep[:cap], reap the tail
|
|
425
|
+
reaped = 0
|
|
426
|
+
for _dt, wid in dead[cap:]:
|
|
427
|
+
if _tmux(tmux, "kill-window", "-t", wid).returncode == 0:
|
|
428
|
+
reaped += 1
|
|
429
|
+
return reaped
|
|
430
|
+
except Exception:
|
|
431
|
+
return 0
|
|
432
|
+
|
|
433
|
+
|
|
376
434
|
def _spawn_fleet_tab(cmd: str) -> tuple[bool, str]:
|
|
377
435
|
"""Run `cmd` as a TAB (tmux window) of the shared fleet window.
|
|
378
436
|
|
|
@@ -399,6 +457,11 @@ def _spawn_fleet_tab(cmd: str) -> tuple[bool, str]:
|
|
|
399
457
|
if r.returncode != 0: # lost a create race -> join as a tab
|
|
400
458
|
fresh_session = False
|
|
401
459
|
if not fresh_session:
|
|
460
|
+
# Sweep-on-launch: reap accumulated dead panes BEFORE adding this
|
|
461
|
+
# agent's tab, so the fleet bar reflects live agents (not the 51
|
|
462
|
+
# cadavers Samuel saw) and a respawn reuses the slot (task 22fcdb21).
|
|
463
|
+
# Only ever touches already-dead windows — never a live agent.
|
|
464
|
+
_reap_dead_fleet_windows(tmux)
|
|
402
465
|
r = _tmux(tmux, "new-window", "-d", "-t", f"={_FLEET_SESSION}:",
|
|
403
466
|
"-n", label, "-P", "-F", "#{window_id}", wrapped)
|
|
404
467
|
if r.returncode != 0:
|
|
@@ -501,6 +564,19 @@ def _spawn_fleet_tab(cmd: str) -> tuple[bool, str]:
|
|
|
501
564
|
_FLEET_NATIVE_DISABLED_TTL_S = 1800 # back off after a watcher "fallback"
|
|
502
565
|
|
|
503
566
|
|
|
567
|
+
def _harden_launcher_dir(d) -> None:
|
|
568
|
+
"""macOS TCC fix (task e0465408) security guardrail: keep the launcher dir
|
|
569
|
+
owner-only (0o700). The `.command`/watcher scripts here are executed by
|
|
570
|
+
LaunchServices via `open -a` and may carry non-secret launch flags; they must
|
|
571
|
+
never be world-readable/-writable (was 0o755). Secrets are NEVER written into
|
|
572
|
+
these scripts (they come via .mcp.json / keychain / env-file). Best-effort:
|
|
573
|
+
a chmod failure must not wedge a launch."""
|
|
574
|
+
try:
|
|
575
|
+
os.chmod(str(d), 0o700)
|
|
576
|
+
except Exception:
|
|
577
|
+
pass
|
|
578
|
+
|
|
579
|
+
|
|
504
580
|
def _fleet_native_paths() -> dict:
|
|
505
581
|
d = Path.home() / ".meshcode" / "launchers"
|
|
506
582
|
return {
|
|
@@ -654,10 +730,11 @@ while :; do
|
|
|
654
730
|
done
|
|
655
731
|
'''
|
|
656
732
|
p["dir"].mkdir(parents=True, exist_ok=True)
|
|
733
|
+
_harden_launcher_dir(p["dir"]) # owner-only launcher dir (TCC fix e0465408)
|
|
657
734
|
driver = p["dir"] / "fleet-native-driver.applescript"
|
|
658
735
|
driver.write_text(_FLEET_NATIVE_DRIVER_AS + "\n", encoding="utf-8")
|
|
659
736
|
p["watcher"].write_text(body, encoding="utf-8")
|
|
660
|
-
os.chmod(p["watcher"],
|
|
737
|
+
os.chmod(p["watcher"], 0o700)
|
|
661
738
|
return p["watcher"]
|
|
662
739
|
|
|
663
740
|
|
|
@@ -737,8 +814,9 @@ def _write_fleet_native_agent(cmd: str) -> Path:
|
|
|
737
814
|
'esac',
|
|
738
815
|
]
|
|
739
816
|
p["dir"].mkdir(parents=True, exist_ok=True)
|
|
817
|
+
_harden_launcher_dir(p["dir"]) # owner-only launcher dir (TCC fix e0465408)
|
|
740
818
|
script_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
741
|
-
os.chmod(script_path,
|
|
819
|
+
os.chmod(script_path, 0o700)
|
|
742
820
|
return script_path
|
|
743
821
|
|
|
744
822
|
|
|
@@ -963,8 +1041,9 @@ def _spawn_terminal_macos(cmd: str) -> tuple[bool, str]:
|
|
|
963
1041
|
lines.append('fi')
|
|
964
1042
|
try:
|
|
965
1043
|
launch_dir.mkdir(parents=True, exist_ok=True)
|
|
1044
|
+
_harden_launcher_dir(launch_dir) # owner-only launcher dir (TCC fix e0465408)
|
|
966
1045
|
script_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
967
|
-
os.chmod(script_path,
|
|
1046
|
+
os.chmod(script_path, 0o700)
|
|
968
1047
|
except Exception as e:
|
|
969
1048
|
return False, f"could not write launcher {script_path}: {e}"
|
|
970
1049
|
# Clear any stale start-marker so the poll below can only see a FRESH one
|
|
@@ -704,6 +704,16 @@ def ensure_boot_env(mcp_json_path, verbose: bool = True) -> Optional[str]:
|
|
|
704
704
|
if env_block.get("MESHCODE_EXPECTED_VERSION") != target:
|
|
705
705
|
env_block["MESHCODE_EXPECTED_VERSION"] = target
|
|
706
706
|
changed = True
|
|
707
|
+
# urgent-wake default-on rollout (task 780d7397): setup_clients bakes
|
|
708
|
+
# MESHCODE_URGENT_WAKE=1 only for FRESH `meshcode setup`. Existing agents'
|
|
709
|
+
# .mcp.json is never regenerated on `meshcode run`, so the env-gated
|
|
710
|
+
# feature would stay OFF forever on installed fleets. Backfill it here —
|
|
711
|
+
# the one place that mutates an existing server env every boot — but only
|
|
712
|
+
# when the key is ABSENT, so an explicit user opt-out (set to "0") is
|
|
713
|
+
# never clobbered.
|
|
714
|
+
if "MESHCODE_URGENT_WAKE" not in env_block:
|
|
715
|
+
env_block["MESHCODE_URGENT_WAKE"] = "1"
|
|
716
|
+
changed = True
|
|
707
717
|
if changed:
|
|
708
718
|
tmp = mcp_json_path.with_name(mcp_json_path.name + ".tmp")
|
|
709
719
|
tmp.write_text(json.dumps(doc, indent=2), encoding="utf-8")
|
|
@@ -215,6 +215,14 @@ def _build_server_block(project: str, project_id: str, agent: str, role: str,
|
|
|
215
215
|
# Mark this Python process as the MCP serve subprocess so other
|
|
216
216
|
# parts of the package (e.g. self_update) skip auto-update inside it.
|
|
217
217
|
"MESHCODE_MCP_SERVE": "1",
|
|
218
|
+
# Urgent-interrupt nudging (task 780d7397, Samuel 2026-06-22): a
|
|
219
|
+
# priority='urgent' message must INTERRUPT a busy agent and be read
|
|
220
|
+
# at once, not merely queue until its next meshcode_wait. This opt-in
|
|
221
|
+
# gates the urgent-only path in server._try_auto_wake (tmux ESC+inject
|
|
222
|
+
# when in a tmux pane, AppleScript/etc fallback otherwise). Deliberately
|
|
223
|
+
# urgent-only — we do NOT set MESHCODE_AUTO_WAKE so non-urgent traffic
|
|
224
|
+
# never injects keystrokes (that stays the conservative default OFF).
|
|
225
|
+
"MESHCODE_URGENT_WAKE": "1",
|
|
218
226
|
}
|
|
219
227
|
if editor_type:
|
|
220
228
|
env["MESHCODE_EDITOR_TYPE"] = editor_type
|
|
@@ -70,11 +70,13 @@ tests/test_core.py
|
|
|
70
70
|
tests/test_cross_agent_messaging.py
|
|
71
71
|
tests/test_date_parse.py
|
|
72
72
|
tests/test_doctor.py
|
|
73
|
+
tests/test_ensure_boot_env_urgent_wake.py
|
|
73
74
|
tests/test_epistemic_v1_python_sdk.py
|
|
74
75
|
tests/test_epistemic_v1_stop_conditions.py
|
|
75
76
|
tests/test_esc_deaf_state.py
|
|
76
77
|
tests/test_exceptions.py
|
|
77
78
|
tests/test_file_upload.py
|
|
79
|
+
tests/test_fleet_reaper.py
|
|
78
80
|
tests/test_helper_visuals.py
|
|
79
81
|
tests/test_hostd_launch_pinned_env.py
|
|
80
82
|
tests/test_hostd_serve_discovery_split.py
|
|
@@ -87,6 +89,7 @@ tests/test_live_mesh_guard.py
|
|
|
87
89
|
tests/test_mark_read_batch.py
|
|
88
90
|
tests/test_marketplace_ratings.py
|
|
89
91
|
tests/test_migration_integrity.py
|
|
92
|
+
tests/test_no_appleevents_on_sweep.py
|
|
90
93
|
tests/test_preflight_hb_gate.py
|
|
91
94
|
tests/test_pretrust_claude.py
|
|
92
95
|
tests/test_push_guard.py
|
|
@@ -113,4 +116,5 @@ tests/test_task_progress.py
|
|
|
113
116
|
tests/test_terminal_lifecycle.py
|
|
114
117
|
tests/test_up_launch_cmd.py
|
|
115
118
|
tests/test_update_guard.py
|
|
119
|
+
tests/test_urgent_wake_tmux.py
|
|
116
120
|
tests/test_wait_open_tasks_contradiction.py
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ensure_boot_env backfills MESHCODE_URGENT_WAKE (task 780d7397 / 2.11.157)
|
|
3
|
+
=========================================================================
|
|
4
|
+
setup_clients bakes MESHCODE_URGENT_WAKE=1 only for a FRESH `meshcode setup`.
|
|
5
|
+
An already-installed agent's .mcp.json is never regenerated on `meshcode run`
|
|
6
|
+
(run_agent only writes it when MISSING), so the env-gated urgent-wake feature
|
|
7
|
+
would stay OFF forever on existing fleets — exactly the gap that would have made
|
|
8
|
+
the front-end smoke fail.
|
|
9
|
+
|
|
10
|
+
ensure_boot_env is the one place that mutates an existing server env block every
|
|
11
|
+
boot. It must backfill the flag when ABSENT, but must NEVER clobber an explicit
|
|
12
|
+
user opt-out (value "0").
|
|
13
|
+
|
|
14
|
+
These tests stub the version-resolution surface so no network / env-pin happens;
|
|
15
|
+
they exercise only the .mcp.json mutation.
|
|
16
|
+
"""
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
22
|
+
|
|
23
|
+
import meshcode.self_update as su
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _write_mcp(tmp_path, env_block):
|
|
27
|
+
p = tmp_path / ".mcp.json"
|
|
28
|
+
doc = {
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"meshcode-x": {
|
|
31
|
+
"command": "/fake/envs/9.9.9/bin/python3",
|
|
32
|
+
"args": ["-m", "meshcode.meshcode_mcp", "serve"],
|
|
33
|
+
"env": env_block,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
p.write_text(json.dumps(doc, indent=2), encoding="utf-8")
|
|
38
|
+
return p
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _stub_versions(monkeypatch, target="9.9.9"):
|
|
42
|
+
"""No-network: current env already == target, so no env-pin path runs."""
|
|
43
|
+
monkeypatch.setattr(su, "update_disabled", lambda: False)
|
|
44
|
+
monkeypatch.setattr(su, "is_editable_install", lambda: False)
|
|
45
|
+
monkeypatch.setattr(su, "resolve_target_version", lambda: target)
|
|
46
|
+
monkeypatch.setattr(su, "_env_version", lambda cmd: target)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _read_env(p):
|
|
50
|
+
doc = json.loads(p.read_text(encoding="utf-8"))
|
|
51
|
+
return next(iter(doc["mcpServers"].values()))["env"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_backfills_when_absent(tmp_path, monkeypatch):
|
|
55
|
+
_stub_versions(monkeypatch)
|
|
56
|
+
p = _write_mcp(tmp_path, {"MESHCODE_EXPECTED_VERSION": "9.9.9"})
|
|
57
|
+
out = su.ensure_boot_env(p, verbose=False)
|
|
58
|
+
assert out == "9.9.9"
|
|
59
|
+
assert _read_env(p)["MESHCODE_URGENT_WAKE"] == "1"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_does_not_clobber_explicit_optout(tmp_path, monkeypatch):
|
|
63
|
+
_stub_versions(monkeypatch)
|
|
64
|
+
p = _write_mcp(tmp_path, {
|
|
65
|
+
"MESHCODE_EXPECTED_VERSION": "9.9.9",
|
|
66
|
+
"MESHCODE_URGENT_WAKE": "0",
|
|
67
|
+
})
|
|
68
|
+
su.ensure_boot_env(p, verbose=False)
|
|
69
|
+
assert _read_env(p)["MESHCODE_URGENT_WAKE"] == "0", "user opt-out was clobbered"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_preserves_existing_on_value(tmp_path, monkeypatch):
|
|
73
|
+
_stub_versions(monkeypatch)
|
|
74
|
+
p = _write_mcp(tmp_path, {
|
|
75
|
+
"MESHCODE_EXPECTED_VERSION": "9.9.9",
|
|
76
|
+
"MESHCODE_URGENT_WAKE": "1",
|
|
77
|
+
})
|
|
78
|
+
su.ensure_boot_env(p, verbose=False)
|
|
79
|
+
assert _read_env(p)["MESHCODE_URGENT_WAKE"] == "1"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_idempotent_second_boot_no_rewrite(tmp_path, monkeypatch):
|
|
83
|
+
_stub_versions(monkeypatch)
|
|
84
|
+
p = _write_mcp(tmp_path, {"MESHCODE_EXPECTED_VERSION": "9.9.9"})
|
|
85
|
+
su.ensure_boot_env(p, verbose=False)
|
|
86
|
+
mtime1 = p.stat().st_mtime_ns
|
|
87
|
+
# Second boot: everything already in place -> changed=False -> no rewrite.
|
|
88
|
+
su.ensure_boot_env(p, verbose=False)
|
|
89
|
+
assert p.stat().st_mtime_ns == mtime1, "rewrote .mcp.json with nothing to change"
|
|
90
|
+
assert _read_env(p)["MESHCODE_URGENT_WAKE"] == "1"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
import pytest
|
|
95
|
+
sys.exit(pytest.main([__file__, "-q"]))
|