meshcode 2.11.166__tar.gz → 2.11.167__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.166 → meshcode-2.11.167}/PKG-INFO +1 -1
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/__init__.py +1 -1
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/run_agent.py +45 -1
- meshcode-2.11.167/meshcode/terminal_mirror_runner.py +478 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode.egg-info/SOURCES.txt +1 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/pyproject.toml +1 -1
- {meshcode-2.11.166 → meshcode-2.11.167}/README.md +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/__main__.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/_launch_smoke.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/_update_guard.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/cli.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/compat.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/daemon.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/doctor.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/hooks/push_guard.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/hostd.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/invites.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/launcher.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/preferences.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/secrets.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/self_update.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/up.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode/upload.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/setup.cfg +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_core.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_doctor.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_ensure_boot_env_urgent_wake.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_fleet_reaper.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_hostd_launch_pinned_env.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_hostd_serve_discovery_split.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_hostd_zombie_sessions.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_launch_smoke.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_live_mesh_guard.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_no_appleevents_on_sweep.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_preflight_hb_gate.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_pretrust_claude.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_prompt_dedup_budget.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_push_guard.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_replica_base_workspace_fallback.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_replica_boot_protocol_unconditional.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_rm_guard.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_session_replay_gate.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_stop_ghost_terminal.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_task_progress.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_terminal_lifecycle.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_up_launch_cmd.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_update_guard.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_urgent_wake_tmux.py +0 -0
- {meshcode-2.11.166 → meshcode-2.11.167}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -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
|
|
@@ -1906,7 +1944,13 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
1906
1944
|
_rc = 0
|
|
1907
1945
|
sys.exit(_rc)
|
|
1908
1946
|
else:
|
|
1909
|
-
# Unix: replace this process with the editor
|
|
1947
|
+
# Unix: replace this process with the editor.
|
|
1948
|
+
# M1 (d1720f2a): when the terminal mirror is enabled, first exec a
|
|
1949
|
+
# thin PTY wrapper that tees the editor's REAL terminal to the owner
|
|
1950
|
+
# dashboard (mc_terminal_frame_push, mig 674). FAIL-OPEN — if the
|
|
1951
|
+
# wrap is disabled or fails for any reason this returns and the
|
|
1952
|
+
# original execvp below runs unchanged (launch is never blocked).
|
|
1953
|
+
_maybe_exec_under_terminal_mirror(cmd, agent, resolved_project)
|
|
1910
1954
|
os.execvp(cmd[0], cmd)
|
|
1911
1955
|
except FileNotFoundError:
|
|
1912
1956
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|