The caller's tmux socket is advertised in the instructions but never targeted: an MCP inside `tmux -L work` queries `default`
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
調査の方向性
Start in src/libtmux_mcp/_utils.py at _get_server(), _get_caller_identity(), and _effective_socket_path(), then inspect server_tools.py and _build_instructions() in server.py. Reproduce with a non-default tmux socket and run the existing test suite before adding coverage for socket targeting, caller detection, instruction coherence, and the environment snapshot race. Done means the queried socket and advertised socket agree and caller detection remains correct.
索引モデルが issue の本文から書いたものです。
説明
Summary
_build_instructions() parses $TMUX at import time to tell the agent "Agent context: this MCP runs inside tmux pane %N (socket work)". That socket name is cosmetic. _get_server() resolves the target socket only from LIBTMUX_SOCKET / LIBTMUX_SOCKET_PATH — never from $TMUX. With neither env var set it constructs a bare Server(), which targets tmux's default socket.
So an MCP server launched inside tmux -L work will:
- advertise
(socket work)in its instructions, - query
defaulton every single tool call, - compute
is_caller: falseon every pane (the check demands a confirmed realpath socket match, and the caller's pane isn't ondefault), - and therefore make the instructions' own advertised recovery — "filter
list_panesforis_caller=true" — return nothing.
The agent is told where it is, told how to confirm it, and both are wrong. Every self-referential and destructive-guard code path silently degrades.
Evidence
Advertised (display only):
server.py#L195—_build_instructionsreadsos.environ["TMUX_PANE"]server.py#L201— hand-parses$TMUXto recover the socket name for the prose line
Targeted (the real behaviour):
_utils.py#L502—_get_server()consults onlyLIBTMUX_SOCKET/LIBTMUX_SOCKET_PATH_utils.py#L523— with neither set, a bareServer()→socket_nameandsocket_pathbothNone→ tmux resolvesdefault
The consequence:
_utils.py#L186—_compute_is_callerrequires_caller_is_strictly_on_server, a realpath socket match. Caller is onwork, panes are ondefault→Falsefor every row.
Upstream contributor: libtmux's socket_path auto-derivation in Server.__init__ is unreachable dead code — its conditions are mutually contradictory, so Server(socket_name=...) and bare Server() always leave socket_path is None:
This is exactly why libtmux-mcp had to grow _effective_socket_path() (_utils.py#L191), whose fallback shells out to tmux display-message -p '#{socket_path}'.
Reproduction
- Start a non-default tmux server and a shell in it:
tmux -L work new-session -d -s repro - Attach, and from inside a pane on that server launch an MCP client whose libtmux-mcp server inherits the environment (
$TMUXwill point at theworksocket;LIBTMUX_SOCKETunset). - Inspect the server instructions delivered at
initialize.- Observed:
Agent context: this MCP runs inside tmux pane %N (socket work).
- Observed:
- Call
list_panes().- Observed: panes from the
defaultsocket (or[]if no default server exists) — not theworkserver the agent is actually sitting in. - Observed:
is_callerisfalseon every returned row. - Expected: panes from
work, with exactly one rowis_caller: true.
- Observed: panes from the
- Teardown:
tmux -L work kill-server.
Suggested fix
1. Make _get_server() follow the caller's socket. When LIBTMUX_SOCKET / LIBTMUX_SOCKET_PATH are unset and $TMUX is present, default to the caller's socket path:
caller = _get_caller_identity()
if caller is not None and caller.socket_path:
return Server(socket_path=caller.socket_path) # -S <path>
Using the socket path (-S) rather than the name (-L) sidesteps both libtmux's unreachable socket_path derivation and the macOS/launchd TMUX_TMPDIR divergence. Server(socket_path=...) is supported and re-injects -S on every command:
libtmux/server.py#L172(constructor)libtmux/server.py#L346(Server.cmd)libtmux/neo.py#L725(fetch_objsre-derives the flags independently)
If the behaviour change is a concern, gate it behind LIBTMUX_FOLLOW_TMUX_SOCKET=1 initially — but note that the current behaviour is already incoherent with what the server tells the agent, so "no change" is not a neutral option.
2. Failing that, stop advertising a socket the tools don't use. If _get_server() will keep defaulting to default, the instructions must not claim (socket work). Say (querying socket default; set LIBTMUX_SOCKET to target your own).
3. Fix ServerInfo so the effective socket is knowable. get_server_info echoes server.socket_name / server.socket_path, which are both None for a bare Server() — even against a live server. Populate them from _effective_socket_path() so the agent can at least discover which socket it just queried.
Bonus defect found while investigating: os.environ["TMUX"] race
libtmux's new_session deletes os.environ["TMUX"] process-globally (restoring it in a finally) so a nested new-session doesn't inherit the parent server:
fastmcp runs sync tools in a threadpool (run_in_thread=True by default, never overridden here):
And _get_caller_identity() re-reads os.environ on every call rather than snapshotting at startup:
So a list_panes or kill_server running concurrently with a create_session can observe TMUX unset — degrading is_caller to None and, more seriously, stripping the self-kill guard's primary socket signal.
Fix: snapshot CallerIdentity once at process start. It cannot change for the life of the process, so there is no reason to re-read the environment per call — and doing so removes the race entirely.
Tests
- With
LIBTMUX_SOCKETunset and$TMUXpointing at a non-default socket, assert_get_server()targets that socket (notdefault). - On a non-default caller socket, assert
list_panes()returns exactly one row withis_caller is True. - Assert the instructions' advertised socket equals the socket
_get_server()actually resolves (a coherence test — this is the invariant that is broken today). - Race regression: with
_get_caller_identitysnapshotted, assert it still returns the caller's identity whileos.environ["TMUX"]is transiently deleted.
Related
- #19 —
is_callerfalse positives across sockets. This is the mirror-image defect: #19 was false positives; this is false negatives on every row. - Companion bug:
filters={"is_caller": true}silently returns[]. - Companion feature:
whoami— which, if built onServer(socket_path=caller.socket_path), is immune to this bug by construction. - Upstream libtmux: unreachable
socket_pathderivation inServer.__init__.
Environment
- libtmux-mcp: 0.1.0a17
- libtmux: 0.61.0
- fastmcp: 3.4.3 / mcp: 1.28.1
- tmux: 3.7b
- Python: 3.14.0
Cross-references — filed together from a single audit of v0.1.0a17. The self-location gap is the root cause; the rest are what it exposed.
- tmux-python/libtmux-mcp#92 — [bug] documented self-location recipe is unexecutable (
filters={"is_caller": true}→[]) - tmux-python/libtmux-mcp#93 — [feat]
where_am_iself-location primitive + self-first relational routing (root fix) - tmux-python/libtmux-mcp#94 — [bug] caller's socket advertised but never targeted;
TMUXenv race vs. the threadpool - tmux-python/libtmux-mcp#95 — [perf]
list_*unbounded, unscoped, unprojected, excluded from the response limiter - tmux-python/libtmux-mcp#96 — [feat] invocation context (in tmux? server running?) + explicit tmux-server auto-start
- tmux-python/libtmux#704 — upstream: no self-location API (
TMUX_PANE); unreachablesocket_pathderivation
(← this issue: #94)
- 主要言語
- Python
- スター
- 13
- フォーク
- 0
- 平均マージ
- 22分
- マージ済み PR(30日)
- 4
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
tmux-python/libtmux-mcp のほかの issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
tmux-python/libtmux-mcp#121 ·
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
tmux-python/libtmux-mcp#120 ·
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
tmux-python/libtmux-mcp#119 ·
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
tmux-python/libtmux-mcp#118 ·
-
pytest filterwarnings suppresses the DeprecationWarnings this project needs to see対応中かも @tony が 89 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
tmux-python/libtmux-mcp#111 ·
tmux-python/libtmux-mcp の issue をすべて見る
似ている issue
-
Validate non-divisible effective batch sizes in the Unsloth LoRA backend対応中かも @debesh-biswas が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
-
難易度 1/5 1時間未満 初心者へのやさしさ 88/100
reflex-dev/reflex#7634 · コメント 1 件 ·
メンテナーはふだん 2 日以内に返信
-
documentation
難易度 1/5 1〜3時間 初心者へのやさしさ 82/100
topoteretes/cognee#6239 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
ranaroussi/yfinance#3009 ·
メンテナーはふだん 1 日以内に返信
-
Test discovery aborts on GPU builds without cuStateVec: check_cuStateVec expects a failed result but set_config() now raises RuntimeError対応中かも @dmeliksetian が今日担当しました。 オープンbug
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
Qiskit/qiskit-aer#2495 ·
メンテナーはふだん 1 日以内に返信