meshcode 2.11.109__tar.gz → 2.11.110rc1__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.109 → meshcode-2.11.110rc1}/PKG-INFO +1 -1
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/__init__.py +1 -1
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/_stop_hook_template.py +8 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/comms_v4.py +33 -4
- meshcode-2.11.110rc1/meshcode/hooks/__init__.py +7 -0
- meshcode-2.11.110rc1/meshcode/hooks/repo_path_lock.py +156 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/hostd.py +92 -12
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/server.py +43 -23
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/protocol_handler.py +36 -6
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/run_agent.py +115 -5
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/setup_clients.py +26 -1
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode.egg-info/SOURCES.txt +2 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/pyproject.toml +2 -1
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/README.md +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/__main__.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/_session_handoff_template 2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/_session_handoff_template 3.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/claude_update 2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/claude_update 3.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/cli.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/compat.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/daemon.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/doctor.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/hostd 2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/invites.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/launcher.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/preferences.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/secrets.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/self_update.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/up 2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/up.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/upload.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/setup.cfg +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_autonomous_prompt_inject 2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_autonomous_prompt_inject 3.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_core.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_doctor.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110rc1}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -48,6 +48,7 @@ reported commander trapped in this exact path. Two new release paths:
|
|
|
48
48
|
connected") → human-confirmed unreachable.
|
|
49
49
|
"""
|
|
50
50
|
import json
|
|
51
|
+
import os
|
|
51
52
|
import sys
|
|
52
53
|
from pathlib import Path
|
|
53
54
|
|
|
@@ -469,6 +470,13 @@ def _user_explicitly_reports_mcp_failure(transcript_path, lookback_records=_UNRE
|
|
|
469
470
|
|
|
470
471
|
|
|
471
472
|
def main():
|
|
473
|
+
# MESHCODE_AGENT_SESSION gate (task 24e3dd44): this hook is safe to install at USER
|
|
474
|
+
# scope (~/.claude/settings.json) because it NO-OPS for any Claude session that is not
|
|
475
|
+
# a meshcode agent launch. run_agent exports MESHCODE_AGENT_SESSION=1 for every agent;
|
|
476
|
+
# a human's personal `claude` session has it unset -> the loop hook never traps them.
|
|
477
|
+
# (Workspace-scope installs also inherit the env, so agent loop-survival is unchanged.)
|
|
478
|
+
if not os.environ.get("MESHCODE_AGENT_SESSION"):
|
|
479
|
+
sys.exit(0)
|
|
472
480
|
raw = sys.stdin.read()
|
|
473
481
|
try:
|
|
474
482
|
payload = json.loads(raw) if raw else {}
|
|
@@ -1797,8 +1797,15 @@ def _heartbeat_loop_pidfile(project, name):
|
|
|
1797
1797
|
return Path.home() / ".meshcode" / f"heartbeat_{project}_{name}.pid"
|
|
1798
1798
|
|
|
1799
1799
|
|
|
1800
|
-
def _start_heartbeat_daemon(project, name):
|
|
1801
|
-
"""Fork a tiny background process that POSTs mc_heartbeat every 30s.
|
|
1800
|
+
def _start_heartbeat_daemon(project, name, agent_pid=None):
|
|
1801
|
+
"""Fork a tiny background process that POSTs mc_heartbeat every 30s.
|
|
1802
|
+
|
|
1803
|
+
Fix A (task 24e3dd44, mesh-dev): when agent_pid is given (the long-lived claude
|
|
1804
|
+
PID = connect_terminal ppid), the fork verifies that process is ALIVE before each
|
|
1805
|
+
POST and self-exits when gone — so a dead agent stops showing 'online'. Windows-
|
|
1806
|
+
SAFE: never os.kill(pid,0) on win32 (that TERMINATES); uses OpenProcess +
|
|
1807
|
+
GetExitCodeProcess (259 == STILL_ACTIVE).
|
|
1808
|
+
"""
|
|
1802
1809
|
pid_file = _heartbeat_loop_pidfile(project, name)
|
|
1803
1810
|
pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1804
1811
|
# Stop any existing heartbeat
|
|
@@ -1818,7 +1825,24 @@ def _start_heartbeat_daemon(project, name):
|
|
|
1818
1825
|
f"project={project!r}; name={name!r}\n"
|
|
1819
1826
|
f"url={SUPABASE_URL!r}; key={SUPABASE_KEY!r}\n"
|
|
1820
1827
|
f"api_key={_ak!r}\n"
|
|
1828
|
+
f"agent_pid={agent_pid!r}\n"
|
|
1821
1829
|
"import urllib.parse\n"
|
|
1830
|
+
"def agent_alive():\n"
|
|
1831
|
+
" if not agent_pid: return True\n"
|
|
1832
|
+
" if sys.platform=='win32':\n"
|
|
1833
|
+
" try:\n"
|
|
1834
|
+
" import ctypes\n"
|
|
1835
|
+
" h=ctypes.windll.kernel32.OpenProcess(0x1000, False, int(agent_pid))\n"
|
|
1836
|
+
" if not h: return False\n"
|
|
1837
|
+
" code=ctypes.c_ulong()\n"
|
|
1838
|
+
" ok=ctypes.windll.kernel32.GetExitCodeProcess(h, ctypes.byref(code))\n"
|
|
1839
|
+
" ctypes.windll.kernel32.CloseHandle(h)\n"
|
|
1840
|
+
" return bool(ok) and code.value==259\n"
|
|
1841
|
+
" except Exception: return True\n"
|
|
1842
|
+
" try:\n"
|
|
1843
|
+
" os.kill(int(agent_pid),0); return True\n"
|
|
1844
|
+
" except ProcessLookupError: return False\n"
|
|
1845
|
+
" except Exception: return True\n"
|
|
1822
1846
|
"def post(path, body):\n"
|
|
1823
1847
|
" req=urllib.request.Request(url+path, data=json.dumps(body).encode(),\n"
|
|
1824
1848
|
" headers={'apikey':key,'Authorization':'Bearer '+key,'Content-Type':'application/json','Accept-Profile':'meshcode','Content-Profile':'meshcode'}, method='POST')\n"
|
|
@@ -1856,6 +1880,8 @@ def _start_heartbeat_daemon(project, name):
|
|
|
1856
1880
|
" except Exception: return True\n"
|
|
1857
1881
|
"pid=get_pid_for_project()\n"
|
|
1858
1882
|
"while True:\n"
|
|
1883
|
+
" if not agent_alive():\n"
|
|
1884
|
+
" sys.exit(0)\n"
|
|
1859
1885
|
" if pid:\n"
|
|
1860
1886
|
" if not check_still_leased(pid):\n"
|
|
1861
1887
|
" sys.exit(0)\n"
|
|
@@ -1961,7 +1987,9 @@ def connect_terminal(project, name, role=""):
|
|
|
1961
1987
|
except Exception:
|
|
1962
1988
|
pass # Non-critical — don't block agent boot
|
|
1963
1989
|
|
|
1964
|
-
|
|
1990
|
+
# Fix A: hand the heartbeat fork the owning claude/agent PID so it self-exits when
|
|
1991
|
+
# the agent dies (instead of keeping a dead agent 'online'). ppid is that long-lived process.
|
|
1992
|
+
hb_pid = _start_heartbeat_daemon(project, name, agent_pid=ppid)
|
|
1965
1993
|
|
|
1966
1994
|
pending = (rpc_result or {}).get("pending_messages", 0)
|
|
1967
1995
|
print(f"[meshcode] Connected: {name} -> {project}")
|
|
@@ -3382,6 +3410,7 @@ if __name__ == "__main__":
|
|
|
3382
3410
|
proj_override = flags.get("project")
|
|
3383
3411
|
editor_override = flags.get("editor")
|
|
3384
3412
|
perm_override = flags.get("permission") # bypass | safe | ask
|
|
3413
|
+
repo_override = flags.get("repo") # repo-scoped launch (cwd=repo + repo-lock hooks); task 24e3dd44
|
|
3385
3414
|
if "--bypass" in sys.argv:
|
|
3386
3415
|
perm_override = "bypass"
|
|
3387
3416
|
elif "--safe" in sys.argv:
|
|
@@ -3413,7 +3442,7 @@ if __name__ == "__main__":
|
|
|
3413
3442
|
autonomous = bool(flags.get("autonomous")) or autonomous_env
|
|
3414
3443
|
import importlib
|
|
3415
3444
|
_run = importlib.import_module("meshcode.run_agent").run
|
|
3416
|
-
sys.exit(_run(agent, project=proj_override, editor_override=editor_override, permission_override=perm_override, dry_run=dry_run, autonomous=autonomous))
|
|
3445
|
+
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))
|
|
3417
3446
|
|
|
3418
3447
|
elif cmd == "scan":
|
|
3419
3448
|
# meshcode scan — detect identicon from stdin/clipboard and launch agent
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""MeshCode packaged Claude Code hook scripts (shipped as package data).
|
|
2
|
+
|
|
3
|
+
Currently: repo_path_lock.py — the canonical PreToolUse repo-path lock that
|
|
4
|
+
hard-locks a repo-scoped agent session to its registered repo even under
|
|
5
|
+
--dangerously-skip-permissions. Installed to ~/.claude/hooks by
|
|
6
|
+
run_agent.ensure_repo_lock_hook_installed() (fallback user-scope path).
|
|
7
|
+
"""
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""MeshCode canonical PreToolUse repo-path lock hook.
|
|
3
|
+
|
|
4
|
+
Hard-locks an agent's filesystem tool calls to its registered repo path —
|
|
5
|
+
EVEN under `--dangerously-skip-permissions` (bypassPermissions). This works
|
|
6
|
+
because Claude Code fires PreToolUse hooks BEFORE any permission-mode check,
|
|
7
|
+
and a hook returning `permissionDecision: "deny"` blocks the tool in every
|
|
8
|
+
mode. settings.json `permissions.deny` does NOT survive bypass — only this hook does.
|
|
9
|
+
|
|
10
|
+
Env contract (exported by the launcher):
|
|
11
|
+
MESHCODE_REPO_LOCK absolute repo root the session is locked to.
|
|
12
|
+
UNSET/empty -> hook is a NO-OP (fail-open).
|
|
13
|
+
MESHCODE_REPO_LOCK_ALLOW optional os.pathsep-separated extra allowed roots.
|
|
14
|
+
MESHCODE_REPO_LOCK_BASH "1" to also scan Bash commands (heuristic).
|
|
15
|
+
Always-allowed: locked root + ALLOW entries + ~/.claude/projects + OS temp.
|
|
16
|
+
"""
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
|
|
23
|
+
_CYGDRIVE_RE = re.compile(r"^/cygdrive/([a-zA-Z])(?=/|$)")
|
|
24
|
+
_MSYS_DRIVE_RE = re.compile(r"^/([a-zA-Z])(?=/|$)")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _demsys(path: str) -> str:
|
|
28
|
+
path = _CYGDRIVE_RE.sub(lambda m: m.group(1) + ":", path)
|
|
29
|
+
return _MSYS_DRIVE_RE.sub(lambda m: m.group(1) + ":", path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _norm(p: str) -> str:
|
|
33
|
+
return os.path.normcase(os.path.realpath(p))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _resolve(path: str, cwd: str) -> str:
|
|
37
|
+
path = os.path.expanduser(_demsys(path))
|
|
38
|
+
cwd = os.path.expanduser(_demsys(cwd))
|
|
39
|
+
if not os.path.isabs(path):
|
|
40
|
+
path = os.path.join(cwd, path)
|
|
41
|
+
return _norm(path)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _within(target: str, root: str) -> bool:
|
|
45
|
+
if target == root:
|
|
46
|
+
return True
|
|
47
|
+
return target.startswith(root + os.sep)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _allowed_roots(cwd: str):
|
|
51
|
+
roots = []
|
|
52
|
+
lock = os.environ.get("MESHCODE_REPO_LOCK", "").strip()
|
|
53
|
+
if lock:
|
|
54
|
+
roots.append(_norm(lock))
|
|
55
|
+
extra = os.environ.get("MESHCODE_REPO_LOCK_ALLOW", "").strip()
|
|
56
|
+
if extra:
|
|
57
|
+
for r in extra.split(os.pathsep):
|
|
58
|
+
r = r.strip()
|
|
59
|
+
if r:
|
|
60
|
+
roots.append(_norm(r))
|
|
61
|
+
home = os.path.expanduser("~")
|
|
62
|
+
roots.append(_norm(os.path.join(home, ".claude", "projects")))
|
|
63
|
+
roots.append(_norm(tempfile.gettempdir()))
|
|
64
|
+
return roots
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_PATH_KEYS = {
|
|
68
|
+
"Read": ["file_path"],
|
|
69
|
+
"Edit": ["file_path"],
|
|
70
|
+
"Write": ["file_path"],
|
|
71
|
+
"MultiEdit": ["file_path"],
|
|
72
|
+
"NotebookEdit": ["notebook_path"],
|
|
73
|
+
"Glob": ["path"],
|
|
74
|
+
"Grep": ["path"],
|
|
75
|
+
"LS": ["path"],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _deny(reason: str) -> None:
|
|
80
|
+
print(json.dumps({
|
|
81
|
+
"hookSpecificOutput": {
|
|
82
|
+
"hookEventName": "PreToolUse",
|
|
83
|
+
"permissionDecision": "deny",
|
|
84
|
+
"permissionDecisionReason": reason,
|
|
85
|
+
}
|
|
86
|
+
}))
|
|
87
|
+
sys.exit(0)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _candidate_bash_paths(command: str):
|
|
91
|
+
toks = re.findall(r'"[^"]*"|\'[^\']*\'|\S+', command)
|
|
92
|
+
for t in toks:
|
|
93
|
+
t = t.strip('"\'')
|
|
94
|
+
if not t:
|
|
95
|
+
continue
|
|
96
|
+
looks_pathy = (
|
|
97
|
+
bool(_MSYS_DRIVE_RE.match(t))
|
|
98
|
+
or bool(_CYGDRIVE_RE.match(t))
|
|
99
|
+
or t.startswith("~/")
|
|
100
|
+
or t.startswith("..")
|
|
101
|
+
or "/.." in t
|
|
102
|
+
or "\\.." in t
|
|
103
|
+
or (len(t) >= 3 and t[1] == ":" and t[2] in "\\/")
|
|
104
|
+
)
|
|
105
|
+
if looks_pathy:
|
|
106
|
+
yield t
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main() -> int:
|
|
110
|
+
try:
|
|
111
|
+
raw = sys.stdin.read()
|
|
112
|
+
data = json.loads(raw) if raw.strip() else {}
|
|
113
|
+
except Exception:
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
if not os.environ.get("MESHCODE_REPO_LOCK", "").strip():
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
tool = data.get("tool_name") or ""
|
|
120
|
+
tool_input = data.get("tool_input") or {}
|
|
121
|
+
cwd = data.get("cwd") or os.getcwd()
|
|
122
|
+
roots = _allowed_roots(cwd)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
if tool in _PATH_KEYS:
|
|
126
|
+
for key in _PATH_KEYS[tool]:
|
|
127
|
+
raw_path = tool_input.get(key)
|
|
128
|
+
if not raw_path:
|
|
129
|
+
continue
|
|
130
|
+
target = _resolve(str(raw_path), cwd)
|
|
131
|
+
if not any(_within(target, r) for r in roots):
|
|
132
|
+
_deny(
|
|
133
|
+
f"repo-path lock: {tool} target '{raw_path}' is outside "
|
|
134
|
+
f"the locked repo. Allowed root: "
|
|
135
|
+
f"{os.environ.get('MESHCODE_REPO_LOCK')}"
|
|
136
|
+
)
|
|
137
|
+
elif tool == "Bash" and os.environ.get("MESHCODE_REPO_LOCK_BASH", "").strip() == "1":
|
|
138
|
+
command = str(tool_input.get("command") or "")
|
|
139
|
+
for cand in _candidate_bash_paths(command):
|
|
140
|
+
target = _resolve(cand, cwd)
|
|
141
|
+
if not any(_within(target, r) for r in roots):
|
|
142
|
+
_deny(
|
|
143
|
+
f"repo-path lock: Bash command references '{cand}' outside "
|
|
144
|
+
f"the locked repo ({os.environ.get('MESHCODE_REPO_LOCK')}). "
|
|
145
|
+
f"Set MESHCODE_REPO_LOCK_BASH=0 to disable bash scanning."
|
|
146
|
+
)
|
|
147
|
+
except SystemExit:
|
|
148
|
+
raise
|
|
149
|
+
except Exception:
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
sys.exit(main())
|
|
@@ -34,6 +34,7 @@ from __future__ import annotations
|
|
|
34
34
|
|
|
35
35
|
import json
|
|
36
36
|
import os
|
|
37
|
+
import re
|
|
37
38
|
import shlex
|
|
38
39
|
import shutil
|
|
39
40
|
import signal as _signal
|
|
@@ -327,7 +328,26 @@ def _meshcode_bin() -> str:
|
|
|
327
328
|
return "meshcode"
|
|
328
329
|
|
|
329
330
|
|
|
330
|
-
def
|
|
331
|
+
def _validate_repo_path(rp) -> Optional[str]:
|
|
332
|
+
"""Validate a per-agent repo override (task 24e3dd44 #4, mc_agents.repo_path).
|
|
333
|
+
Returns an absolute realpath dir, or None. REJECTS (None + logs): NULL/empty, a value with a
|
|
334
|
+
double-quote or control char (injection hardening), or a path that is not an existing dir."""
|
|
335
|
+
if not rp or not isinstance(rp, str):
|
|
336
|
+
return None
|
|
337
|
+
if '"' in rp or any(ord(ch) < 0x20 for ch in rp):
|
|
338
|
+
_log(f"WARN: repo_path rejected (quote/control char): {rp!r}")
|
|
339
|
+
return None
|
|
340
|
+
try:
|
|
341
|
+
p = os.path.realpath(os.path.expanduser(rp))
|
|
342
|
+
except Exception:
|
|
343
|
+
return None
|
|
344
|
+
if not os.path.isdir(p):
|
|
345
|
+
_log(f"WARN: repo_path rejected (not a directory): {rp!r} -> {p}")
|
|
346
|
+
return None
|
|
347
|
+
return p
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _spawn_agent(project: str, agent: str, headless: bool = False, repo_path=None) -> bool:
|
|
331
351
|
"""Relaunch `meshcode run <project>/<agent>`.
|
|
332
352
|
|
|
333
353
|
headless=False (default): VISIBLE terminal window. Samuel must SEE it — we
|
|
@@ -338,9 +358,14 @@ def _spawn_agent(project: str, agent: str, headless: bool = False) -> bool:
|
|
|
338
358
|
|
|
339
359
|
headless=True (Fleet Control mig404 per-agent flag): background process, NO
|
|
340
360
|
window — for fleet agents that don't need a terminal (like the qa launch).
|
|
361
|
+
|
|
362
|
+
repo_path (task 24e3dd44 #4): optional per-agent repo. When set + valid, `--repo <path>` is
|
|
363
|
+
appended so `meshcode run` opens there. NULL/invalid = unchanged. Inert until mc_agents.repo_path
|
|
364
|
+
+ the respawn RPC carry it.
|
|
341
365
|
"""
|
|
342
366
|
target = f"{project}/{agent}"
|
|
343
367
|
bin_ = _meshcode_bin()
|
|
368
|
+
repo = _validate_repo_path(repo_path)
|
|
344
369
|
if headless:
|
|
345
370
|
# background, NO terminal — UNIVERSAL macOS/Linux/Windows (task c1a6c6a8, mesh-dev specs).
|
|
346
371
|
# Clean env (a stale CLAUDECODE aborts `meshcode run`); keep crash logs in a per-agent logfile
|
|
@@ -403,7 +428,8 @@ def _spawn_agent(project: str, agent: str, headless: bool = False) -> bool:
|
|
|
403
428
|
try:
|
|
404
429
|
# `python -m meshcode` (NOT the meshcode.exe shim) so the .exe isn't held open by the
|
|
405
430
|
# agent -> a background `pip install -U` can replace it on Windows (task 14782bb4 #4).
|
|
406
|
-
|
|
431
|
+
argv = [sys.executable, "-m", "meshcode", "run", target] + (["--repo", repo] if repo else [])
|
|
432
|
+
proc = subprocess.Popen(argv, **kwargs)
|
|
407
433
|
# LAUNCH-NO-WINDOW (task 35bee961): record the headless PID so the Stop sweep
|
|
408
434
|
# (_do_stops) can hard-kill it — a headless agent has NO window for Samuel to
|
|
409
435
|
# close, so desired_state='stopped' must be enforced by killing the process.
|
|
@@ -435,14 +461,17 @@ def _spawn_agent(project: str, agent: str, headless: bool = False) -> bool:
|
|
|
435
461
|
except Exception:
|
|
436
462
|
_bindir = ""
|
|
437
463
|
if sys.platform == "win32":
|
|
464
|
+
# repo already validated (no '"'/control chars, real dir) -> safe to double-quote.
|
|
465
|
+
_repo_win = f' --repo "{repo}"' if repo else ''
|
|
438
466
|
cmd = (f'set "CLAUDECODE=" & set "CLAUDE_CODE_SESSION=" & '
|
|
439
467
|
f'set "MESHCODE_NO_UPDATE=" & set "MESHCODE_NO_AUTO_UPDATE=" & '
|
|
440
468
|
+ (f'set "PATH={_bindir};%PATH%" & ' if _bindir else '')
|
|
441
|
-
+ f'"{sys.executable}" -m meshcode run "{target}"')
|
|
469
|
+
+ f'"{sys.executable}" -m meshcode run "{target}"{_repo_win}')
|
|
442
470
|
else:
|
|
471
|
+
_repo_posix = f" --repo {shlex.quote(repo)}" if repo else ''
|
|
443
472
|
cmd = (f"unset CLAUDECODE CLAUDE_CODE_SESSION MESHCODE_NO_UPDATE MESHCODE_NO_AUTO_UPDATE; "
|
|
444
473
|
+ (f"export PATH={shlex.quote(_bindir)}:$PATH; " if _bindir else "")
|
|
445
|
-
+ f"exec {shlex.quote(sys.executable)} -m meshcode run {shlex.quote(target)}")
|
|
474
|
+
+ f"exec {shlex.quote(sys.executable)} -m meshcode run {shlex.quote(target)}{_repo_posix}")
|
|
446
475
|
try:
|
|
447
476
|
from meshcode import protocol_handler as _ph
|
|
448
477
|
ok, info = _ph._spawn_terminal(cmd)
|
|
@@ -567,7 +596,7 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
567
596
|
_old_vis_pids = _discover_agent_pids(_target) if (_is_recycle and _visible) else []
|
|
568
597
|
_log(f"{'RECYCLE-RESPAWN' if _is_recycle else 'RESPAWN'} {proj}/{agent} "
|
|
569
598
|
f"({'VISIBLE ' if _visible else ''}stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
|
|
570
|
-
if _spawn_agent(proj, agent, headless=_hl):
|
|
599
|
+
if _spawn_agent(proj, agent, headless=_hl, repo_path=c.get("repo_path")):
|
|
571
600
|
_record_spawn(_target) # count the terminal we just opened, against the breaker
|
|
572
601
|
if _is_recycle:
|
|
573
602
|
if _visible and _old_vis_pids:
|
|
@@ -839,8 +868,14 @@ def _pid_cmdline(pid: int) -> str:
|
|
|
839
868
|
def _discover_agent_pids(target: str) -> list:
|
|
840
869
|
"""Fallback PID discovery by command line, for agents spawned before this hostd
|
|
841
870
|
(no recorded PID) or after a state-file loss. Matches `meshcode run <target>`.
|
|
842
|
-
Best-effort; returns [] on any failure.
|
|
871
|
+
Best-effort; returns [] on any failure.
|
|
872
|
+
|
|
873
|
+
SAFETY (prefix-match fix, task 24e3dd44): target must match as a WHOLE token — `run <target>`
|
|
874
|
+
followed by whitespace/end/quote — NEVER a bare substring, else 'back' matches 'back-2' and a
|
|
875
|
+
downstream reaper/force-kill could KILL a healthy sibling. re.escape keeps target literal. On
|
|
876
|
+
POSIX pgrep -f is substring, so re-verify each candidate's full cmdline with the exact regex."""
|
|
843
877
|
pids = []
|
|
878
|
+
tok = re.compile(r"run\s+" + re.escape(target) + r"(?=\s|$|[\"'])")
|
|
844
879
|
try:
|
|
845
880
|
if sys.platform == "win32":
|
|
846
881
|
out = subprocess.run(
|
|
@@ -850,7 +885,7 @@ def _discover_agent_pids(target: str) -> list:
|
|
|
850
885
|
for line in out.splitlines():
|
|
851
886
|
line = line.strip()
|
|
852
887
|
if line.startswith("CommandLine="):
|
|
853
|
-
block_pid = ("meshcode" in line and
|
|
888
|
+
block_pid = ("meshcode" in line and bool(tok.search(line)))
|
|
854
889
|
elif line.startswith("ProcessId=") and block_pid:
|
|
855
890
|
try:
|
|
856
891
|
pids.append(int(line.split("=", 1)[1]))
|
|
@@ -863,14 +898,52 @@ def _discover_agent_pids(target: str) -> list:
|
|
|
863
898
|
capture_output=True, text=True, timeout=8).stdout
|
|
864
899
|
for ln in out.split():
|
|
865
900
|
try:
|
|
866
|
-
|
|
901
|
+
p = int(ln)
|
|
867
902
|
except Exception:
|
|
868
|
-
|
|
903
|
+
continue
|
|
904
|
+
# pgrep matched a substring; keep ONLY if the full cmdline has the exact token.
|
|
905
|
+
if tok.search(_pid_cmdline(p)):
|
|
906
|
+
pids.append(p)
|
|
869
907
|
except Exception:
|
|
870
908
|
pass
|
|
871
909
|
return pids
|
|
872
910
|
|
|
873
911
|
|
|
912
|
+
def _kill_heartbeat_fork(target: str) -> None:
|
|
913
|
+
"""Kill the agent's heartbeat daemon fork (Fix B, task 24e3dd44). The fork is detached into its
|
|
914
|
+
OWN session, so os.killpg(getpgid(agent_pid)) in _kill_headless_pid does NOT take it down — it
|
|
915
|
+
would keep POSTing and show a stopped agent 'online'. Stop it by its pidfile
|
|
916
|
+
heartbeat_<proj>_<name>.pid. Best-effort."""
|
|
917
|
+
try:
|
|
918
|
+
proj, _, agent = target.partition("/")
|
|
919
|
+
if not agent:
|
|
920
|
+
return
|
|
921
|
+
pidf = STATE_DIR / f"heartbeat_{proj}_{agent}.pid"
|
|
922
|
+
if not pidf.exists():
|
|
923
|
+
return
|
|
924
|
+
try:
|
|
925
|
+
hb = int(pidf.read_text(encoding="utf-8").strip())
|
|
926
|
+
except Exception:
|
|
927
|
+
hb = 0
|
|
928
|
+
if hb:
|
|
929
|
+
try:
|
|
930
|
+
if sys.platform == "win32":
|
|
931
|
+
subprocess.run(["taskkill", "/PID", str(hb), "/F"], capture_output=True, timeout=8)
|
|
932
|
+
else:
|
|
933
|
+
os.kill(hb, _signal.SIGTERM)
|
|
934
|
+
except ProcessLookupError:
|
|
935
|
+
pass
|
|
936
|
+
except Exception:
|
|
937
|
+
pass
|
|
938
|
+
try:
|
|
939
|
+
pidf.unlink()
|
|
940
|
+
except Exception:
|
|
941
|
+
pass
|
|
942
|
+
_log(f"STOP {target}: stopped heartbeat fork (pid {hb or '?'})")
|
|
943
|
+
except Exception:
|
|
944
|
+
pass
|
|
945
|
+
|
|
946
|
+
|
|
874
947
|
def _kill_headless_pid(target: str, pid: int) -> bool:
|
|
875
948
|
"""Hard-kill a recorded headless PID + its child tree. Guards against PID reuse by
|
|
876
949
|
confirming the cmdline still looks like this meshcode agent. Returns True if killed."""
|
|
@@ -905,6 +978,7 @@ def _kill_headless_pid(target: str, pid: int) -> bool:
|
|
|
905
978
|
except Exception:
|
|
906
979
|
pass
|
|
907
980
|
_log(f"STOP {target}: hard-killed headless pid {pid}")
|
|
981
|
+
_kill_heartbeat_fork(target) # Fix B: its detached heartbeat fork won't die with the killpg
|
|
908
982
|
return True
|
|
909
983
|
except Exception as e:
|
|
910
984
|
_log(f"WARN: STOP {target} kill pid {pid} failed: {e}")
|
|
@@ -980,8 +1054,11 @@ def _do_force_kills(api_key: str, host_id: str) -> int:
|
|
|
980
1054
|
if not pid:
|
|
981
1055
|
continue
|
|
982
1056
|
if _FORCE_KILL_DRYRUN:
|
|
1057
|
+
# SAFE-ARM logging: log the cmdline so the would-kill can be validated as a real
|
|
1058
|
+
# stuck visible agent before _FORCE_KILL_DRYRUN is flipped.
|
|
983
1059
|
_log(f"FORCE-KILL-DRYRUN {target}: WOULD kill visible pid {pid} (explicit human stop, "
|
|
984
|
-
f"hb {a.get('heartbeat_age_s')}s)
|
|
1060
|
+
f"hb {a.get('heartbeat_age_s')}s) cmdline={_pid_cmdline(pid).strip()[:100]!r} — "
|
|
1061
|
+
f"log-only. Flip _FORCE_KILL_DRYRUN=False to enforce.")
|
|
985
1062
|
elif _kill_headless_pid(target, pid):
|
|
986
1063
|
pids.pop(target, None)
|
|
987
1064
|
n += 1
|
|
@@ -1037,8 +1114,11 @@ def _do_reap(api_key: str, host_id: str) -> int:
|
|
|
1037
1114
|
"""LOG-ONLY while _REAP_DRYRUN; else hard-kill (reuse-guard inside). Returns 1 if killed."""
|
|
1038
1115
|
nonlocal pids
|
|
1039
1116
|
if _REAP_DRYRUN:
|
|
1040
|
-
|
|
1041
|
-
|
|
1117
|
+
# SAFE-ARM logging: include the cmdline of what we WOULD kill so it can be validated as a
|
|
1118
|
+
# true ghost/orphan — NOT a healthy agent — before flip.
|
|
1119
|
+
_log(f"REAP-DRYRUN {target}: WOULD kill pid {pid} ({reason}) "
|
|
1120
|
+
f"cmdline={_pid_cmdline(pid).strip()[:100]!r} — log-only, no kill. "
|
|
1121
|
+
f"Flip _REAP_DRYRUN=False ONLY after confirming zero false positives.")
|
|
1042
1122
|
return 0
|
|
1043
1123
|
if _kill_headless_pid(target, pid):
|
|
1044
1124
|
if pids.get(target) == pid:
|
|
@@ -4324,31 +4324,51 @@ def _mark_realtime_msgs_read_in_db(messages: List[Dict[str, Any]]) -> None:
|
|
|
4324
4324
|
if not api_key:
|
|
4325
4325
|
return
|
|
4326
4326
|
|
|
4327
|
-
#
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4327
|
+
# RE-DELIVERY FIX (2.11.110, task 01465452 / Samuel+commander 2026-06-04):
|
|
4328
|
+
# mark_read IS the read-watermark. If it silently fails under DB load, the
|
|
4329
|
+
# messages stay read=false and meshcode_wait RE-DELIVERS them next cycle
|
|
4330
|
+
# (commander hit this 3-4x this session). Retry transient failures with a
|
|
4331
|
+
# short backoff; SURFACE (WARNING) any IDs still unmarked instead of
|
|
4332
|
+
# swallowing at debug — "exactly-once" delivery depends on this completing.
|
|
4333
|
+
def _retry_rpc(fn, attempts=3, base=0.2):
|
|
4334
|
+
last = None
|
|
4335
|
+
for i in range(attempts):
|
|
4336
|
+
try:
|
|
4337
|
+
r = fn()
|
|
4338
|
+
if not (isinstance(r, dict) and be.is_error(r)):
|
|
4339
|
+
return r, None
|
|
4340
|
+
last = be.get_error_message(r)
|
|
4341
|
+
except Exception as e:
|
|
4342
|
+
last = str(e)
|
|
4343
|
+
if i < attempts - 1:
|
|
4344
|
+
_time.sleep(base * (i + 1)) # 0.2s, 0.4s — transient timeout/contention
|
|
4345
|
+
return None, last
|
|
4346
|
+
|
|
4347
|
+
# Try batch first (1 RPC for N messages), retried on transient failure.
|
|
4348
|
+
result, err = _retry_rpc(lambda: be.sb_rpc("mc_mark_messages_read_batch", {
|
|
4349
|
+
"p_api_key": api_key,
|
|
4350
|
+
"p_project_id": _PROJECT_ID,
|
|
4351
|
+
"p_message_ids": msg_ids,
|
|
4352
|
+
}))
|
|
4353
|
+
if result is not None:
|
|
4354
|
+
log.debug(f"batch mark_read: {result.get('marked', 0)}/{len(msg_ids)} marked")
|
|
4355
|
+
return
|
|
4356
|
+
log.debug(f"batch mark_read failed after retries ({err}); falling back to individual")
|
|
4341
4357
|
|
|
4342
|
-
# Fallback: individual RPCs
|
|
4358
|
+
# Fallback: individual RPCs, each retried. Track stragglers so a persistent
|
|
4359
|
+
# failure is VISIBLE (re-delivery is the lesser evil vs silent message loss).
|
|
4360
|
+
unmarked = []
|
|
4343
4361
|
for mid in msg_ids:
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4362
|
+
_r, _e = _retry_rpc(lambda mid=mid: be.sb_rpc("mc_mark_message_read", {
|
|
4363
|
+
"p_api_key": api_key,
|
|
4364
|
+
"p_project_id": _PROJECT_ID,
|
|
4365
|
+
"p_message_id": mid,
|
|
4366
|
+
}), attempts=2)
|
|
4367
|
+
if _r is None:
|
|
4368
|
+
unmarked.append(mid)
|
|
4369
|
+
if unmarked:
|
|
4370
|
+
log.warning(f"mark_read: {len(unmarked)}/{len(msg_ids)} msgs UNMARKED after retries "
|
|
4371
|
+
f"(will re-deliver) — likely DB-load timeout: {unmarked[:5]}")
|
|
4352
4372
|
|
|
4353
4373
|
# Confirm delivery (best-effort, background) — completes the
|
|
4354
4374
|
# delivery pipeline: sent → read → confirmed.
|
|
@@ -257,8 +257,14 @@ def _spawn_terminal(cmd: str) -> tuple[bool, str]:
|
|
|
257
257
|
# ============================================================
|
|
258
258
|
# launch-batch
|
|
259
259
|
# ============================================================
|
|
260
|
-
def cmd_launch_batch(agent_names: Iterable[str]) -> int:
|
|
261
|
-
"""meshcode launch-batch <agent_names...> — spawn one terminal each.
|
|
260
|
+
def cmd_launch_batch(agent_names: Iterable[str], repo_path: Optional[str] = None) -> int:
|
|
261
|
+
"""meshcode launch-batch <agent_names...> [--repo <path>] — spawn one terminal each.
|
|
262
|
+
|
|
263
|
+
repo_path (optional, core-commander launch-diff 2.11.110): if set, each agent
|
|
264
|
+
launches repo-scoped (`meshcode run <name> --repo <path>` → cwd=repo + repo-lock
|
|
265
|
+
hooks). Validated AT THIS SINK (realpath + isdir + reject '"'/control chars)
|
|
266
|
+
BEFORE shell/cmd.exe interpolation, since repo_path is USER input. Empty/None
|
|
267
|
+
=> unscoped (100% legacy behavior).
|
|
262
268
|
|
|
263
269
|
Returns 0 on full success, 1 if any agent failed to launch (still
|
|
264
270
|
prints JSON summary to stdout).
|
|
@@ -275,6 +281,18 @@ def cmd_launch_batch(agent_names: Iterable[str]) -> int:
|
|
|
275
281
|
# Resolve `meshcode` binary path (CLI wrapper installed by pip).
|
|
276
282
|
mc_bin = shutil.which("meshcode") or "meshcode"
|
|
277
283
|
|
|
284
|
+
# SECURITY (audit P1-4): repo_path is USER input that gets interpolated into the
|
|
285
|
+
# platform launch string below. Validate AT THE SINK: canonicalize, require a
|
|
286
|
+
# real dir, reject the double-quote + control chars that could break out of the
|
|
287
|
+
# cmd.exe / shell quoting. None/empty => rp=None => unscoped (legacy behavior).
|
|
288
|
+
rp: Optional[str] = None
|
|
289
|
+
if repo_path:
|
|
290
|
+
rp = os.path.realpath(os.path.expanduser(repo_path))
|
|
291
|
+
if (not os.path.isdir(rp)) or ('"' in rp) or any(ord(c) < 32 for c in rp):
|
|
292
|
+
print(json.dumps({"ok": False, "error": f"invalid repo path: {repo_path!r}",
|
|
293
|
+
"error_code": "invalid_repo_path"}))
|
|
294
|
+
return 1
|
|
295
|
+
|
|
278
296
|
# RATE LIMIT: hard-cap the batch so a crafted `agents=` list can't spawn an
|
|
279
297
|
# unbounded number of terminals (DoS). Excess is reported, never launched.
|
|
280
298
|
if len(names) > _MAX_BATCH:
|
|
@@ -309,9 +327,9 @@ def cmd_launch_batch(agent_names: Iterable[str]) -> int:
|
|
|
309
327
|
# PER-PLATFORM quoting (mesh-core FIX2): cmd.exe wants double-quotes, not POSIX shlex
|
|
310
328
|
# single-quotes (cmd.exe passes single-quotes through literally -> file-not-found).
|
|
311
329
|
if sys.platform == "win32":
|
|
312
|
-
cmd = f'"{mc_bin}" run "{name}"'
|
|
330
|
+
cmd = f'"{mc_bin}" run "{name}"' + (f' --repo "{rp}"' if rp else "")
|
|
313
331
|
else:
|
|
314
|
-
cmd = f"{shlex.quote(mc_bin)} run {shlex.quote(name)}"
|
|
332
|
+
cmd = f"{shlex.quote(mc_bin)} run {shlex.quote(name)}" + (f" --repo {shlex.quote(rp)}" if rp else "")
|
|
315
333
|
ok, info = _spawn_terminal(cmd)
|
|
316
334
|
if ok:
|
|
317
335
|
launched.append(name)
|
|
@@ -404,7 +422,12 @@ def cmd_launch_url(url: str) -> int:
|
|
|
404
422
|
"error_code": "invalid_input"}))
|
|
405
423
|
return 1
|
|
406
424
|
|
|
407
|
-
|
|
425
|
+
# core-commander launch-diff: optional repo= scopes every launched agent to a
|
|
426
|
+
# repo (cwd=repo + repo-lock hooks). parse_qs ALREADY URL-decodes the value, so
|
|
427
|
+
# no extra unquote here (double-decode would corrupt a path with a literal '%').
|
|
428
|
+
# Path is validated at the sink in cmd_launch_batch.
|
|
429
|
+
repo = qs.get("repo", [""])[0].strip()
|
|
430
|
+
return cmd_launch_batch(agents, repo_path=repo or None)
|
|
408
431
|
|
|
409
432
|
|
|
410
433
|
# ============================================================
|
|
@@ -543,7 +566,14 @@ def main(argv: list[str]) -> int:
|
|
|
543
566
|
sub = argv[0]
|
|
544
567
|
rest = argv[1:]
|
|
545
568
|
if sub == "launch-batch":
|
|
546
|
-
|
|
569
|
+
# Optional `--repo <path>` (core-commander launch-diff): pull it out of the
|
|
570
|
+
# positional agent-name list; remaining tokens are agent names.
|
|
571
|
+
_repo = None
|
|
572
|
+
if "--repo" in rest:
|
|
573
|
+
_i = rest.index("--repo")
|
|
574
|
+
_repo = rest[_i + 1] if _i + 1 < len(rest) else None
|
|
575
|
+
rest = rest[:_i] + rest[_i + 2:]
|
|
576
|
+
return cmd_launch_batch(rest, repo_path=_repo)
|
|
547
577
|
if sub == "launch-url":
|
|
548
578
|
if not rest:
|
|
549
579
|
print(json.dumps({"ok": False, "error": "url required",
|
|
@@ -679,7 +679,73 @@ def _preflight_heartbeat(agent: str, project: str) -> None:
|
|
|
679
679
|
print(f"[meshcode] Pre-flight heartbeat skipped: {e}", file=sys.stderr)
|
|
680
680
|
|
|
681
681
|
|
|
682
|
-
|
|
682
|
+
# Repo-scoped launch (task 24e3dd44 / core-commander launch-diff). When `meshcode run
|
|
683
|
+
# <agent> --repo <path>` is used, the agent boots with cwd=repo (not the meshcode
|
|
684
|
+
# workspace), so its repo CLAUDE.md loads — we carry the boot protocol via
|
|
685
|
+
# --append-system-prompt so the mesh loop + rules still apply. .format(agent, project,
|
|
686
|
+
# role, repo) at launch.
|
|
687
|
+
MESHCODE_BOOT_PROTOCOL = """You are agent {agent} in MeshCode meshwork {project}. Role: {role}.
|
|
688
|
+
You are running INSIDE the repo at {repo} (your cwd) — operate here like a normal terminal opened in this repo: read its CLAUDE.md + local memory, work here, and do NOT leave it (a hard repo-path lock denies file ops outside it).
|
|
689
|
+
|
|
690
|
+
ON SESSION START (run NOW, don't idle/greet): if meshcode_* tools are deferred, FIRST call ToolSearch(query="select:meshcode_set_status,meshcode_boot,meshcode_wait,meshcode_send,meshcode_task_complete"). Then 1) meshcode_set_status(status="online",task="ready"); 2) meshcode_boot(); 3) meshcode_wait().
|
|
691
|
+
|
|
692
|
+
PERMANENT LOOP (#1 rule): after boot and after EVERY action, your next tool call MUST be meshcode_wait(). act -> (optional meshcode_send) -> meshcode_wait() -> repeat. NEVER exit/stop without calling meshcode_wait(). Only exits: user says stop/sleep/exit, commander got_done/sleep auth, or fatal error. If wait times out, re-call with 2x timeout (cap 1800s).
|
|
693
|
+
|
|
694
|
+
RULES: MCP tools only (don't shell out to the meshcode CLI). Tasks > messages; messages <100 tokens (signals), no empty acks, JSON, thread via in_reply_to. Close tasks immediately when done (meshcode_task_complete); work assigned tasks immediately. Sync vs async: turn-based/shared-state -> meshcode_call; informing -> meshcode_send. meshcode_remember(key,value) for reusable learnings.
|
|
695
|
+
|
|
696
|
+
ALWAYS USE SKILLS + SUBAGENTS (mandatory): invoke the Skill tool whenever any skill matches (even 1%) BEFORE acting; dispatch subagents for multi-file search/audits. Samuel flagged skipping skills/subagents as underperforming — hard rule."""
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def ensure_repo_lock_hook_installed():
|
|
700
|
+
"""FALLBACK (user-scope) install of the canonical repo-path lock as a PreToolUse hook in
|
|
701
|
+
~/.claude/settings.json. SELF-GATED on MESHCODE_REPO_LOCK (the hook no-ops when unset), so
|
|
702
|
+
installing it globally is safe. Idempotent — never duplicates an existing identical command."""
|
|
703
|
+
try:
|
|
704
|
+
import importlib.resources
|
|
705
|
+
import json as _json
|
|
706
|
+
home = Path(os.path.expanduser("~"))
|
|
707
|
+
hd = home / ".claude" / "hooks"
|
|
708
|
+
hd.mkdir(parents=True, exist_ok=True)
|
|
709
|
+
dst = hd / "repo-path-lock.py"
|
|
710
|
+
dst.write_text((importlib.resources.files("meshcode.hooks") / "repo_path_lock.py").read_text("utf-8"), "utf-8")
|
|
711
|
+
py = sys.executable or "python3"
|
|
712
|
+
settings = home / ".claude" / "settings.json"
|
|
713
|
+
doc = _json.loads(settings.read_text("utf-8")) if settings.exists() else {}
|
|
714
|
+
pre = doc.setdefault("hooks", {}).setdefault("PreToolUse", [])
|
|
715
|
+
cmd = f'"{py}" "{dst}"'
|
|
716
|
+
matcher = "Read|Edit|Write|MultiEdit|NotebookEdit|Glob|Grep|LS|Bash"
|
|
717
|
+
if not any(any(h.get("command") == cmd for h in e.get("hooks", [])) for e in pre):
|
|
718
|
+
pre.append({"matcher": matcher, "hooks": [{"type": "command", "command": cmd}]})
|
|
719
|
+
settings.write_text(_json.dumps(doc, indent=2), "utf-8")
|
|
720
|
+
except Exception:
|
|
721
|
+
pass
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def ensure_loop_hook_installed():
|
|
725
|
+
"""FALLBACK (user-scope) install of stay_on_loop as a Stop hook in ~/.claude/settings.json.
|
|
726
|
+
SAFE globally because STOP_HOOK_BODY now no-ops unless MESHCODE_AGENT_SESSION is set (a human's
|
|
727
|
+
personal claude session never gets trapped). Reuses the canonical STOP_HOOK_BODY. Idempotent."""
|
|
728
|
+
try:
|
|
729
|
+
from meshcode._stop_hook_template import STOP_HOOK_BODY
|
|
730
|
+
import json as _json
|
|
731
|
+
home = Path(os.path.expanduser("~"))
|
|
732
|
+
hd = home / ".claude" / "hooks"
|
|
733
|
+
hd.mkdir(parents=True, exist_ok=True)
|
|
734
|
+
dst = hd / "stay_on_loop.py"
|
|
735
|
+
dst.write_text(STOP_HOOK_BODY, "utf-8")
|
|
736
|
+
py = sys.executable or "python3"
|
|
737
|
+
settings = home / ".claude" / "settings.json"
|
|
738
|
+
doc = _json.loads(settings.read_text("utf-8")) if settings.exists() else {}
|
|
739
|
+
stop = doc.setdefault("hooks", {}).setdefault("Stop", [])
|
|
740
|
+
cmd = f'"{py}" "{dst}"'
|
|
741
|
+
if not any(any(h.get("command") == cmd for h in e.get("hooks", [])) for e in stop):
|
|
742
|
+
stop.append({"hooks": [{"type": "command", "command": cmd}]})
|
|
743
|
+
settings.write_text(_json.dumps(doc, indent=2), "utf-8")
|
|
744
|
+
except Exception:
|
|
745
|
+
pass
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def run(agent: str, project: Optional[str] = None, editor_override: Optional[str] = None, permission_override: Optional[str] = None, dry_run: bool = False, autonomous: bool = False, repo_path: Optional[str] = None) -> int:
|
|
683
749
|
"""Launch the user's editor with ONLY the named agent's MCP server loaded.
|
|
684
750
|
|
|
685
751
|
dry_run: when True, exercise the full bootstrap codepath (auth check,
|
|
@@ -836,6 +902,33 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
836
902
|
except Exception:
|
|
837
903
|
pass
|
|
838
904
|
|
|
905
|
+
# ── Repo-scoped launch + mesh-session hook wiring (task 24e3dd44 / core-commander) ─
|
|
906
|
+
# MESHCODE_AGENT_SESSION marks THIS process as a mesh agent launch — it gates the
|
|
907
|
+
# user-scope stay_on_loop fallback so the loop is enforced for agents but a human's
|
|
908
|
+
# personal `claude` session is NEVER trapped. Exported for EVERY launch (not only
|
|
909
|
+
# repo-scoped) so loop-survival is unconditional.
|
|
910
|
+
os.environ["MESHCODE_AGENT_SESSION"] = "1"
|
|
911
|
+
repo_lock = None
|
|
912
|
+
if repo_path:
|
|
913
|
+
_rp = os.path.realpath(os.path.expanduser(repo_path))
|
|
914
|
+
if (not os.path.isdir(_rp)) or ('"' in _rp) or any(ord(c) < 32 for c in _rp):
|
|
915
|
+
print(json.dumps({"ok": False, "error": f"invalid --repo path: {repo_path!r}",
|
|
916
|
+
"error_code": "invalid_repo_path"}), file=sys.stderr)
|
|
917
|
+
return 2
|
|
918
|
+
repo_lock = _rp
|
|
919
|
+
os.environ["MESHCODE_REPO_LOCK"] = repo_lock
|
|
920
|
+
os.environ.setdefault("MESHCODE_REPO_LOCK_BASH", "1")
|
|
921
|
+
os.environ["MESHCODE_REPO_LOCK_ALLOW"] = os.pathsep.join(
|
|
922
|
+
[str(ws), os.path.expanduser("~/.claude/projects")])
|
|
923
|
+
print(f"[meshcode] Repo-lock: {repo_lock} (file ops outside it are denied)", file=sys.stderr)
|
|
924
|
+
# Pre-stage BOTH user-scope fallback hooks (idempotent). repo-lock self-gates on
|
|
925
|
+
# MESHCODE_REPO_LOCK; loop gates on MESHCODE_AGENT_SESSION — so this is safe even when
|
|
926
|
+
# --settings already carries the workspace hooks (the primary path). One boot test
|
|
927
|
+
# reveals which path fires; both are wired so no recut is needed.
|
|
928
|
+
if not dry_run:
|
|
929
|
+
ensure_repo_lock_hook_installed()
|
|
930
|
+
ensure_loop_hook_installed()
|
|
931
|
+
|
|
839
932
|
# ── Validate stop hook exists (required for wait-loop integrity) ─
|
|
840
933
|
# If the workspace was created on an old CLI that didn't install hooks
|
|
841
934
|
# (or someone wiped .claude/), Claude Code has nothing blocking turn-end
|
|
@@ -929,6 +1022,9 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
929
1022
|
# color synchronously, so the banner gets the real color without any
|
|
930
1023
|
# cross-process coordination.
|
|
931
1024
|
|
|
1025
|
+
# Default so a banner-build failure below can't leave agent_role undefined for the
|
|
1026
|
+
# repo-scoped --append-system-prompt .format() further down (task 24e3dd44).
|
|
1027
|
+
agent_role = "MCP-connected agent"
|
|
932
1028
|
# ── Welcome banner with unique ASCII art + personality ──────────
|
|
933
1029
|
try:
|
|
934
1030
|
from .ascii_art import generate_art, render_welcome
|
|
@@ -1032,13 +1128,27 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
1032
1128
|
# instructions so the agent auto-initializes (set_status, check
|
|
1033
1129
|
# messages, claim tasks, greet peers) without waiting for user input.
|
|
1034
1130
|
# Uses -- to separate options from the positional prompt argument.
|
|
1131
|
+
# Repo-scoped launch (task 24e3dd44 / core-commander): when --repo is set the
|
|
1132
|
+
# session runs at cwd=repo (NOT the workspace), so explicitly load the workspace
|
|
1133
|
+
# .mcp.json + .claude/settings.json (Stop + repo-lock hooks) via flags, and carry
|
|
1134
|
+
# the mesh boot protocol via --append-system-prompt (the repo's own CLAUDE.md
|
|
1135
|
+
# loads from cwd). --repo unset => none of this added => behavior 100% unchanged.
|
|
1136
|
+
if repo_lock:
|
|
1137
|
+
cmd += [
|
|
1138
|
+
"--mcp-config", str(mcp_json_path),
|
|
1139
|
+
"--settings", str(ws / ".claude" / "settings.json"),
|
|
1140
|
+
"--append-system-prompt", MESHCODE_BOOT_PROTOCOL.format(
|
|
1141
|
+
agent=agent, project=resolved_project,
|
|
1142
|
+
role=(agent_role or "MCP-connected agent"), repo=repo_lock),
|
|
1143
|
+
]
|
|
1035
1144
|
cmd.extend(["--", "boot"])
|
|
1036
|
-
|
|
1037
|
-
|
|
1145
|
+
_launch_cwd = repo_lock or str(ws)
|
|
1146
|
+
if not os.path.isdir(_launch_cwd):
|
|
1147
|
+
print(f"[meshcode] WARNING: launch dir does not exist: {_launch_cwd}", file=sys.stderr)
|
|
1038
1148
|
try:
|
|
1039
|
-
os.chdir(
|
|
1149
|
+
os.chdir(_launch_cwd)
|
|
1040
1150
|
except Exception as e:
|
|
1041
|
-
print(f"[meshcode] WARNING: chdir to
|
|
1151
|
+
print(f"[meshcode] WARNING: chdir to {_launch_cwd} failed: {e}", file=sys.stderr)
|
|
1042
1152
|
elif editor_stem == "cursor":
|
|
1043
1153
|
# Cursor reads .cursor/mcp.json from the workspace cwd.
|
|
1044
1154
|
cmd = [editor, str(ws)]
|
|
@@ -24,6 +24,15 @@ from typing import Dict, Any, Optional
|
|
|
24
24
|
|
|
25
25
|
from meshcode.exceptions import AuthError, RPCError, MeshCodeConnectionError
|
|
26
26
|
from meshcode._stop_hook_template import STOP_HOOK_BODY as _STOP_HOOK_BODY
|
|
27
|
+
# Repo-path lock hook body (task 24e3dd44) — read from the packaged canonical hook so the
|
|
28
|
+
# workspace copy + the user-scope fallback (run_agent.ensure_repo_lock_hook_installed) stay
|
|
29
|
+
# byte-identical. Best-effort: empty -> workspace just won't get the primary-path PreToolUse
|
|
30
|
+
# hook (the user-scope fallback still installs it; an empty hook would fail-open anyway).
|
|
31
|
+
try:
|
|
32
|
+
import importlib.resources as _ilr
|
|
33
|
+
_REPO_LOCK_BODY = (_ilr.files("meshcode.hooks") / "repo_path_lock.py").read_text("utf-8")
|
|
34
|
+
except Exception:
|
|
35
|
+
_REPO_LOCK_BODY = ""
|
|
27
36
|
from meshcode._session_handoff_template import (
|
|
28
37
|
HANDOFF_WRITE_BODY as _HANDOFF_WRITE_BODY,
|
|
29
38
|
HANDOFF_READ_BODY as _HANDOFF_READ_BODY,
|
|
@@ -345,12 +354,23 @@ def _meshcode_settings_dict() -> dict:
|
|
|
345
354
|
"""Canonical .claude/settings.json content (Stop + PreCompact +
|
|
346
355
|
SessionStart hooks, all Windows-safe). Single source used by both the
|
|
347
356
|
scaffolder and the patch-hooks fresh-create path."""
|
|
348
|
-
|
|
357
|
+
d = {
|
|
349
358
|
"hooks": {
|
|
350
359
|
event: [{"hooks": [{"type": "command", "command": _hook_command(script)}]}]
|
|
351
360
|
for event, script in _MESHCODE_HOOKS
|
|
352
361
|
},
|
|
353
362
|
}
|
|
363
|
+
# Repo-path lock (task 24e3dd44 / core-commander): PreToolUse needs a MATCHER (the other
|
|
364
|
+
# meshcode hooks are matcher-less), so it's added explicitly rather than via _MESHCODE_HOOKS.
|
|
365
|
+
# The hook self-gates on MESHCODE_REPO_LOCK -> inert for non-repo-scoped sessions, so adding
|
|
366
|
+
# it to every workspace's settings.json is safe. run_agent passes --settings <ws>/.claude/
|
|
367
|
+
# settings.json on a repo-scoped launch so this Stop+PreToolUse pair loads into the cwd=repo
|
|
368
|
+
# session (the PRIMARY path; the user-scope install is the fallback).
|
|
369
|
+
d["hooks"]["PreToolUse"] = [{
|
|
370
|
+
"matcher": "Read|Edit|Write|MultiEdit|NotebookEdit|Glob|Grep|LS|Bash",
|
|
371
|
+
"hooks": [{"type": "command", "command": _hook_command("repo_path_lock.py")}],
|
|
372
|
+
}]
|
|
373
|
+
return d
|
|
354
374
|
|
|
355
375
|
|
|
356
376
|
def _heal_settings_hooks(ws: Path, dry_run: bool = False):
|
|
@@ -1354,6 +1374,10 @@ If `meshcode_wait()` times out, call it again with a 2× longer timeout (cap 180
|
|
|
1354
1374
|
- `sensitive=True` for secrets / PII.
|
|
1355
1375
|
- Memory: `meshcode_remember(key, value)` for reusable learnings. Don't dump
|
|
1356
1376
|
task summaries into memory — tasks already persist.
|
|
1377
|
+
- **ALWAYS USE SKILLS + SUBAGENTS (mandatory):** invoke the Skill tool whenever
|
|
1378
|
+
any skill matches (even 1%) BEFORE acting; dispatch subagents for multi-file
|
|
1379
|
+
search/audits. Samuel flagged skipping skills/subagents as underperforming —
|
|
1380
|
+
hard rule.
|
|
1357
1381
|
|
|
1358
1382
|
## Mobile-first UI verification (before task_complete)
|
|
1359
1383
|
|
|
@@ -1527,6 +1551,7 @@ Call `meshcode_wait` now.
|
|
|
1527
1551
|
("stay_on_loop.py", stop_hook_body),
|
|
1528
1552
|
("session_handoff_write.py", _HANDOFF_WRITE_BODY),
|
|
1529
1553
|
("session_handoff_read.py", _HANDOFF_READ_BODY),
|
|
1554
|
+
("repo_path_lock.py", _REPO_LOCK_BODY), # task 24e3dd44: primary-path repo-lock (PreToolUse)
|
|
1530
1555
|
):
|
|
1531
1556
|
_hp = ws / ".claude" / "hooks" / _name
|
|
1532
1557
|
_hp.write_text(_body, encoding="utf-8")
|
|
@@ -43,6 +43,8 @@ meshcode.egg-info/dependency_links.txt
|
|
|
43
43
|
meshcode.egg-info/entry_points.txt
|
|
44
44
|
meshcode.egg-info/requires.txt
|
|
45
45
|
meshcode.egg-info/top_level.txt
|
|
46
|
+
meshcode/hooks/__init__.py
|
|
47
|
+
meshcode/hooks/repo_path_lock.py
|
|
46
48
|
meshcode/meshcode_mcp/__init__.py
|
|
47
49
|
meshcode/meshcode_mcp/__main__.py
|
|
48
50
|
meshcode/meshcode_mcp/backend.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "meshcode"
|
|
7
|
-
version = "2.11.
|
|
7
|
+
version = "2.11.110rc1"
|
|
8
8
|
description = "Real-time communication between AI agents — Supabase-backed CLI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -51,3 +51,4 @@ include = ["meshcode", "meshcode.*"]
|
|
|
51
51
|
|
|
52
52
|
[tool.setuptools.package-data]
|
|
53
53
|
meshcode = ["comms_v4.py"]
|
|
54
|
+
"meshcode.hooks" = ["*.py"]
|
|
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
|
{meshcode-2.11.109 → meshcode-2.11.110rc1}/meshcode/meshcode_mcp/test_prefs_claude_version.py
RENAMED
|
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
|