Skip to content

Support(fix): Gate unsafe optional tmux features - #780

Draft
tony wants to merge 19 commits into
bump-min-tmux-000from
bump-min-tmux-001
Draft

tony wants to merge 19 commits into
bump-min-tmux-000from
bump-min-tmux-001

Conversation

@tony

@tony tony commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Keep ordinary subprocess operations on tmux 3.3a and qualify optional features separately. This PR stacks on the core minimum change.

Operation Version policy
Ordinary reads, writes, polling and direct named paste-buffer -b NAME -d Keep 3.3a
Named buffer deletion and rename Require 3.4 before dispatch
Captured foreground shell-job stdout Require 3.5 before executing the job; background and pane-targeted jobs retain their native behavior
Literal select-layout on 3.3a Refuse unknown names and malformed serialized headers before the whole batch; retain valid presets and native layout controls
Owned control acquisition Require 3.7 before starting a control client
Single-pane typed break-pane before 3.6 Reread the pane's actual window after successful native relinking, which prints no ID
break-pane on exact 3.7 Refuse unnamed and raw operations; typed explicit naming uses the native crash prevention and subsequent rename repair
Raw percentage split-window -p on 3.4 Refuse the affected release; typed percentages use portable -l N%
Window names containing : or . on exact 3.7 Retain native rejection before creation; other tested supported releases retain those names

Control acquisition validates the actual daemon, reading its version, pid, start time and socket path with the hook-free run-shell -C probe so no user hook runs before the 3.7 check, creates a private hard link to its Unix socket, and checks identity through that link before starting -C. A replaced public socket cannot redirect initialization to an older daemon. The alias remains owned through client registration and is cleaned up with the client resources. A short temporary carrier and same-filesystem peer fallback retain valid long endpoints and cross-device temporary roots. Unsupported socket-link filesystems refuse acquisition. Local native evidence covers Linux/WSL; macOS remains CI qualification.

Literal argv command items and flat batches receive the relevant feature checks before any requested command runs. Exact aliases retain their native meanings, including set for options. Opaque shell/tmux programs and configured alias bodies retain native semantics. Library-owned metadata and guard commands must retain native built-in semantics. Raw client output does not promise captured nested shell-job stdout.

Operational format and flag decisions use the connected daemon rather than the executable's diagnostic version. Readiness checks, output buffering, polling and wire decoding remain necessary. On exact 3.7 a later rename failure can leave the pane moved; the error describes that partial effect. Older supported releases retain ordinary break-pane naming.

Validation:

  • An earlier combined revision passes the full exact-3.7 suite: 1,541 tests and seven skips, without reruns. A preceding combined revision passes the full 3.3a suite with 1,436 tests and 110 skips and the full 3.7d suite with 1,537 tests and nine skips. Those broad runs precede the short-carrier, policy-marker and window-name fixture follow-ups.
  • The short-carrier source passes all 42 control/feature cases on 3.7d with three skips, and the affected pane/server/control/feature scope on 3.4 with 240 passes and 55 skips. Positive fixtures skip only for their own feature requirement; explicit refusal tests still call the actual operation.
  • All 38 native policy cases pass across 3.3a, 3.4, 3.5, exact 3.7 and 3.7d. Actual control ownership, old-daemon refusal and endpoint-replacement witnesses pass. Separate native tests cover a valid long socket and an injected cross-device link failure with owned cleanup.
  • Deliberate regressions make the buffer, captured-output, break, percentage and control gates fail for the intended reasons. Restoring the earlier carrier makes both new carrier tests fail; restoring the earlier native-option matcher fails its alias witness.
  • A follow-up fixes successful single-pane breaks without printed output on 3.3a–3.5. Four native cases cover current and externally moved pane contexts, with and without explicit names. The whole feature test file passes across all five releases, after the original code failed both initial native cases with IndexError. Ruff/mypy pass and an independent review verifies the final four-file delta.
  • Ruff format/lint, mypy and strict Sphinx pass. Independent source reviews cover the combined policy, native aliases, short-carrier lifetime and final backend-marker/name-test delta. Replay preserves the exact reviewed and gated candidate bytes, with isolated changelog commits.
  • The control identity probe, which also used display-message, now uses run-shell -C with comma-separated fields and a single-quoted socket path. A native test shows acquiring a control client fires neither after-display-message nor command-error, and fails when display-message is restored; a second reads a socket path holding quotes, a semicolon, braces, a hash, a dollar sign and a comma. The control, version check and optional-feature modules pass on 3.3a, 3.4, 3.6b, 3.7d, 3.8 and master (control cases skip below 3.7).

libtmux._internal.support.require_commands lets other engines apply this enforcement contract before handing off work; it replaces the integer policy marker, so a consumer imports a function and an older libtmux fails the import. On 3.3a the layout guard raises InvalidLayout, a LibTmuxException that is still a ValueError, instead of a bare ValueError, so callers that catch the library's base class see one type on every release. The final layout follow-up passes the whole native feature file on all five releases (42, 41, 41, 43 and 41 passes respectively, with feature-specific skips) and strict Sphinx, with exact source bytes unchanged. The prior implementation fails all three canonical/alias/prefix refusal regressions. Independent review checks the native option and serialized-header semantics. Native clients 3.3a, 3.4 and 3.5 cannot register control with an exact-3.7 daemon; the same daemon stays alive and a newer compatible client registers. This pairing limit does not raise ordinary support. Hosted CI passes on tmux 3.3a, 3.4, 3.5, 3.6, 3.7, 3.7a, 3.7b and master, and on the 3.2a/3.3 rejection job, at revision 309fdfed (run); the native results above qualify the local Linux/WSL environment.

The CHANGES entry records that Server.run_shell(target_pane=...) now returns None, as its signature documents, instead of an empty list.

Merge and release order: #779 and this PR land first and libtmux 0.63.0 is released from them; libtmux-mcp (#139, #140) and tmuxp (#1092, #1093) require 0.63.0 and merge only after that release. Nothing here tags or releases.

Closes #778.

@codecov

codecov Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.63801% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.56%. Comparing base (f6e4630) to head (5df9cb3).

Files with missing lines Patch % Lines
src/libtmux/_internal/control_mode.py 67.36% 23 Missing and 8 partials ⚠️
src/libtmux/_internal/support/versions.py 92.13% 5 Missing and 2 partials ⚠️
src/libtmux/pane.py 78.57% 2 Missing and 1 partial ⚠️
src/libtmux/_internal/support/__init__.py 50.00% 1 Missing ⚠️
src/libtmux/common.py 80.00% 1 Missing ⚠️
src/libtmux/exc.py 0.00% 1 Missing ⚠️
src/libtmux/pytest_plugin.py 85.71% 1 Missing ⚠️
Additional details and impacted files
@@                  Coverage Diff                  @@
##           bump-min-tmux-000     #780      +/-   ##
=====================================================
+ Coverage              54.24%   55.56%   +1.31%     
=====================================================
  Files                     30       30              
  Lines                   3969     4136     +167     
  Branches                 767      803      +36     
=====================================================
+ Hits                    2153     2298     +145     
- Misses                  1519     1534      +15     
- Partials                 297      304       +7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tony added 15 commits October 17, 2026 07:12
Check literal commands and complete batches before effects. Require 3.4 for named-buffer identity and 3.5 for captured shell-job stdout. Refuse defective native 3.4 percentages and exact 3.7 raw breaks; retain portable typed percentages and explicit-name break repair. Select operational flags from the daemon and expose the backend policy contract.
Require stable daemon 3.7 before starting control mode. Own a private socket hard link, verify daemon identity through it and keep it until client cleanup. Retry short same-filesystem carriers for long paths and cross-device temp roots. Retain baseline fixtures and test native refusals, replacement, option aliases and cleanup.
Document each daemon requirement, native 3.7 name rejection, partial break repair and the optional-policy backend marker. State the Linux/WSL carrier evidence and continuing readiness obligations.
Keep 3.3a and patched releases in the full matrix; add exact 3.7 so native name rejection and break-pane refusal/repair retain coverage.
Record the optional requirements independently of the 3.3a core minimum.
tmux 3.3a through 3.5 completes the sole-pane break path before printing -P, leaving the wrapper to index an empty result after the operation. Refresh the actual pane context on successful empty output and retain nonzero-exit refusal.

Native regression cases cover named and unnamed calls with both current and externally moved pane contexts. The focused feature suite passes on 3.3a, 3.4, 3.5, exact 3.7 and 3.7d; exact 3.7 unnamed refusal remains in force.
Note that a successful single-pane Pane.break_pane() returns its actual window on tmux 3.3a through 3.5, whose native relink path prints no ID. Keep the release note separate from runtime changes.
why: tmux 3.3a can terminate its daemon when select-layout reaches an
invalid header. A raw batch must refuse that operand before its prefix
commands execute.

what:
- Keep unique presets and standard serialized headers on 3.3a
- Preserve native cycling, spread and undo argument handling
- Carry canonical, alias and prefix calls through whole-batch validation
- Advance the backend policy contract to version 2
- Record the native client pairing limit independently of the core floor
ControlMode read the daemon's version, pid, start time and socket path with
display-message -p, which fires a user's after-display-message hook before
the pin or the 3.7 eligibility check could refuse anything. Read the same
fields with `-N run-shell -C`, whose failed parse reports the expanded text
without running an after-* or command-error hook.

The expanded text is parsed as a tmux command, so the fields are comma
separated and the socket path, which may hold any character, is
single-quoted with embedded quotes re-opened. A report spanning several lines
is refused so a newline in the path cannot truncate it.

Tests: a native test sets after-display-message and command-error hooks,
acquires and releases a control client, then fires a control display-message
and a failing command; each sentinel holding exactly one letter shows the
pin fired none. A second test reads a socket path holding quotes, a
semicolon, braces, a hash, a dollar sign and a comma. The Popen trackers
ignore the probe's own -C. The mocked probes in test_optional_features.py
answer in the run-shell format.

Verification: restoring display-message fails the hook test. The control,
version check and optional-feature modules pass on 3.7d; ruff and mypy are clean.
Note that control acquisition runs no user hooks before it validates the endpoint. Keep the release note separate from runtime changes.
On 3.3a an invalid select-layout string was refused with a bare ValueError,
while 3.4 and newer failed the same call with a LibTmuxException, so callers
that catch the library's base class (tmuxp's builder, MCP tool error mapping)
saw a different type only on the floor release. Add InvalidLayout, which is a
LibTmuxException and still a ValueError for existing handlers, and raise it
from the guard.

Test: the 3.3a refusal tests expect InvalidLayout, and a hierarchy test pins
both bases. Restoring the bare ValueError fails the 3.3a refusal cases. The
optional-feature module passes on 3.3a.
Backend consumers identified the optional-feature contract by an integer,
libtmux.common.TMUX_OPTIONAL_FEATURE_POLICY_VERSION, and called the private
_require_commands. Publish version check.require_commands(version, argv,
repaired_break=False) and drop the marker: a consumer that needs the policy
imports the function, and an older libtmux without it fails the import rather
than passing a number check.

Test: the 3.3a refusals for named-buffer deletion and layouts, the 3.4
control, paste-and-delete staying available, and the exact 3.7 break-pane
refusal and its explicit-name repair. Replacing the function body with a
no-op fails the new test. The version check module and its doctest pass on 3.7d.
Replace the policy-version paragraph: programs that run their own tmux
subprocesses apply the optional-feature checks through
libtmux._internal.support.require_commands.
Note InvalidLayout, the public command policy and the return-type change of
Server.run_shell(target_pane=...) from an empty list to None. Keep the release
note separate from runtime changes.
tony added 4 commits October 17, 2026 10:42
ControlMode refused `3.8-rc2` and `next-3.8` because it required a stable
`X.Y[a-z]` report on top of the ranked comparison. Check anything that ranks at
or above 3.7 final under the same strict ordering as version check, and refuse
`next-3.7` and `3.7-rc*`, which rank below it.

Registration also asked `list-clients` for the control client's name, a
command a user can hook. tmux names a client with no tty `client-<pid>`, so
set that name from the spawned process and wait for registration with the
hook-free `run-shell -C '<marker>#{L:#{client_pid},}'` instead.

Tests: a table checks 3.7, 3.7a, 3.7d, 3.8, 3.8-rc, 3.8-rc2, next-3.8 and
next-3.9 and refuses 3.6b, 3.7-rc, 3.7-rc3, next-3.7, 3.3a, master and the
empty string; a live client's name equals tmux's own; the hook test now also
arms after-list-clients, after-list-panes, after-list-sessions and
after-refresh-client and asserts none fired. Restoring the stable-only check
fails four version check cases; restoring the list-clients poll fails the hook
test. The control module passes on 3.7d (27 passed, 1 skipped).
tmux 3.7 rejects `:` and `.` in window and session names; 3.7a reverted it.
libtmux checked only session names, on every release, and left window names to
tmux's own error after the command had started. Check `new-window -n`,
`new-session -s/-n`, `rename-window` and `rename-session` (including aliases,
prefixes and `;`-joined items) before dispatch when the daemon is exactly 3.7,
in the same command policy that guards named buffers and break-pane, so typed
methods, raw commands, batches and the libtmux-mcp/tmuxp paths that call the
policy inherit it. The refusal is a VersionTooLow naming the name.

Tests: nine name commands are refused on 3.7, pass on 3.3a, 3.6b, 3.7a, 3.8 and
3.8-rc2, and targets or formats holding delimiters stay allowed. The native
window and session tests pass on a real exact-3.7 build. Removing the check
fails all nine refusal cases.
The options and hooks page, the filtering comparison and the format-field
docstrings still named tmux 3.2 or 3.2a as the project minimum. State 3.3a,
the supported floor; the 3.3 entries in the format table are always met.
Note the strict control eligibility ranking, local client naming and the
window and session name refusal on tmux 3.7. Keep the release note separate
from runtime changes.
@tony
tony force-pushed the bump-min-tmux-001 branch from 309fdfe to 8907eeb Compare October 17, 2026 16:16
@tony
tony force-pushed the bump-min-tmux-000 branch 2 times, most recently from 71f1bb5 to f6e4630 Compare October 17, 2026 17:39
@tony
tony force-pushed the bump-min-tmux-001 branch from 8907eeb to 5df9cb3 Compare October 17, 2026 17:40

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant