Skip to content

Make tmux operations literal, bounded, and identity-safe - #6

Merged
tony merged 78 commits into
masterfrom
improvements-00
Aug 30, 2026
Merged

Make tmux operations literal, bounded, and identity-safe#6
tony merged 78 commits into
masterfrom
improvements-00

Conversation

@tony

@tony tony commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Literal dispatch: Preserve semicolons, newlines, large input, names, and option values without reinterpretation or truncation.
  • Atomic input: Type command lines and paste text as single tmux operations, including standard-input transport for large payloads.
  • Stable identity: Bind snapshots, handles, and window links to the daemon and session that produced them.
  • Bounded runtime: Bound process and control I/O, waits, subscriptions, protocol messages, workers, and cleanup.
  • Deterministic higher layers: Validate and stage workspaces before effects while making MCP targets, destructive metadata, and publication dependencies accurate.

Changes by area

  • Core: Harden transport, snapshots, options, filters, buffers, waits, and handle lifecycle.
  • Workspace and MCP: Correct workspace rollback and MCP session, watcher, request, and target ownership.
  • Distribution: Strengthen publication, consumer documentation, fixture cleanup, and compatibility gates.

Breaking changes

  • Generic execution modes are removed; process execution remains the default and control mode is explicit.
  • The buffer-name overload becomes pasteBuffer, while paste accepts literal text.
  • CommandRequest carries command groups and optional input instead of one flat argv.

Test plan

  • Net patch and changelog pass git diff --check.
  • ./gradlew check passes all 70 tasks on JDK 21 and JDK 25.
  • Published-artifact verification and tmux 3.2a through 3.7c compatibility checks pass.

tony added 30 commits August 29, 2026 06:31
why: Process and control streams could block indefinitely, lose dispatch
certainty, or leak workers and descendants during timeout and close races.

what:
- Bound admission, output, deadlines, and process reclamation
- Parse control replies on dedicated workers with bounded subscriptions
- Cover interruption, close barriers, malformed bytes, and stream loss
why: Public filter nodes could change after construction, valid relations
were missing from built-in models, and malformed wire documents escaped the
schema error boundary.

what:
- Give built-in fields stable handles and immutable value semantics
- Complete cyclic relation models and operator coverage
- Fail closed on malformed JSON and document Java regex semantics
why: Handles could act on a replacement tmux process, linked windows lost
their session context, and generic execution carriers could change command
meaning after tmux alias expansion.

what:
- Bind snapshots and handles to the live tmux process identity
- Validate hierarchy slots, targets, layouts, and hydration scalars
- Remove execution modes and their benchmark surface
why: Invalid or unsupported workspace input could create a partial session,
and an uncertain creation reply left no exact rollback target.

what:
- Split parsing from application
- Validate names, layouts, topology, and tmux version before creation
- Create under a unique staging name and roll back that exact session
why: Teardown treated a failed probe as proof that tmux had exited, while
abandoned-server recovery left dead owner directories behind.

what:
- Keep abandoned process and directory ownership together
- Delete owner directories only after process exit is confirmed
- Run leak regressions under the Java test root with unconditional cleanup
why: Watch notifications could race or grow without bound, attachment gaps
lost invalidations, and malformed protocol input could leave the launcher
alive after its session ended.

what:
- Serialize bounded invalidations and reconcile every session attachment
- Track pane generations, retry outages with backoff, and hide watcher clients
- Canonicalize resource input, discover live sockets, and couple launcher exit
  to protocol-session closure
why: The documented testTmuxMatrix command did not exist even though every
module registered per-release lane tasks.

what:
- Create one root compatibility task
- Let each matrix-enabled module contribute all supported tmux lanes
why: Safety tiers can be mistaken for confinement even though mutating
tools can run arbitrary commands in a pane.

what:
- State that the ceiling filters tools and protocol annotations
- Name the OS, socket and container boundaries that constrain effects
Snapshot the server identity around hydration and retry one complete
capture when the server is replaced. Preserve real listing and parse
failures when the incarnation remains unchanged.

Run version gates from the captured snapshot and guard winlink
operations with one target-scoped tmux command so a stale session:index
cannot act on its replacement.
Share one bounded process owner between subprocess and control carriers. Snapshot descendants before failure paths can close a pipe, keep later daemonized servers outside cleanup, and report incomplete reclamation without replacing the primary failure.

Start pump workers lazily and redact rejected control argv.
why: Snapshot consistency now samples server identity on both sides of
hydration, so the whoami command-count guard was stale.

what:
- Expect four listings, two identity probes, and one socket lookup
- Keep the guard exact so handle-by-handle traversal still fails
why: The mutating ceiling includes arbitrary commands and input, but its
tools advertised destructiveHint=false, which promises additive-only
updates and can suppress client confirmation.

what:
- Declare each tool's update effect separately from its availability ceiling
- Mark all 16 non-additive tools destructive, including mutating-tier commands
- Verify direct and stdio hints and correct the safety documentation
Fail closed on tmux 3.2a and 3.3a because deleting an absent
named buffer removes the top buffer. Protect trailing semicolons
from tmux's command-group parser.
Use one buffer per call and require tmux 3.4 so failures can
clean up the exact buffer. Move typing coverage out of the
aggregate real-tmux test.
Reject NUL in PaneSpec before workspace topology can change.
Keep cleanup-failure coverage on an independent post-creation
failure.
Send literal text plus carriage return in one tmux operation. Protect option-shaped and semicolon-terminated lines across direct pane and command-chain delivery.
Reuse Pane.sendLine so a payload and its carriage return reach tmux in one operation. Prevent concurrent tmux_run calls on one pane from merging their shell input.
Use stable session IDs for rename and kill, leaving literal server as the only non-ID destructive target. Keep session-name lookup only on arguments that explicitly request names.
Carry exit status in the framed end marker so late completion cannot leave a pane option behind. Keep the shell independent of Java cleanup after timeout or uncertain delivery.
Treat failed protocol output as a disconnect, including broken-pipe errors hidden by PrintStream. Centralize exactly-once session completion across output and transport shutdown while preserving primary failures.
Upgrade to MCP SDK 2.0.1 so unterminated stdio messages have a hard size limit. Pin the bounded constructor with a focused session-lifecycle regression.
Close the watcher, transport, and input when either stdio side ends or startup fails. Signal the launcher before cleanup and preserve primary failures across immediate and graceful shutdown.
Own the tmux Server lexically so failed watcher or protocol setup cannot strand its non-daemon transport threads. Cover the empty-server watch failure through a real child process.
Limit externally signalled waits to all but one process so signal and observation
calls remain dispatchable without increasing the transport bound. Fail excess
waits before dispatch and preserve dispatch certainty across close races.
Stop hierarchy capture after an empty session listing because tmux refuses
child listings without a current target. Keep the observed process identity
and let MCP watch startup report its session requirement.
Run later watcher, attachment, transport, and SDK cleanup even when an earlier
owner fails. Preserve the first failure and suppress later ones through one
package-private cleanup boundary.
Transfer caller-supplied transport ownership on entry and roll it back when
SDK construction fails. Close the built server after acceptance, preserve the
startup failure, and detach any prepared watcher.
why: SDK 2.0.1 rejects concurrent emissions to its unicast stdio
sink, so a response can be lost when progress or watcher output races
it.

what:
- Queue each session's sends without blocking a worker thread
- Prove ordering and failure release with deterministic tests
why: The five-minute override governs server-to-client requests, not
tool calls. SDK 2.0.1 can issue roots/list implicitly, turning a
ten-second upstream wait into five minutes.

what:
- Remove the misdirected five-minute request timeout
- Record the effective ten-second bound without wall-clock delay
why: SDK 2.0.1 does not cancel running synchronous handlers, and
workspace rollback cannot undo commands already started. Existing
prose promised both.

what:
- Distinguish client abandonment from handler termination
- Describe workspace validation and cleanup as best effort
- Trim duplicated wait rationale
tony added 18 commits August 30, 2026 06:13
why: The case starts a replacement server on the socket the one it
killed was using, and tmux answers a client reaching a server that is
still exiting with "server exited unexpectedly" rather than "no server
running", from 3.3a onwards. Under full-matrix load the replacement's
new-session reached the dying server and the case failed for the
teardown rather than for what it is about. It failed on the 3.3a lane
and passed alone on both this commit and the one before it.

what:
- Wait for tmux to unlink the socket before opening the replacement
why: The case makes the shell acknowledge the function definition, but
not the clear between that and the marker it asserts on, so under
full-matrix load the assertion could run against a screen the shell had
not caught up with. It failed that way on the 3.2a lane and passed
alone, which is the second time this case has been load-only.

what:
- Acknowledge the clear the same way the definition is acknowledged
why: Thirteen files each carried the same poll loop with the same
five-second bound, and a lane of the matrix runs the whole suite on a
machine the other lanes are also using. That bound is only ever spent
when something is already wrong, so a tight one reports the load rather
than the library — which is how the literal-line case failed on two
different lanes on two consecutive runs.

what:
- Await.until and Await.output replace thirteen copies, 147 lines fewer
- Raise the bound to fifteen seconds, in the one place it now lives
- Keep the subscription waiter in ExamplesTest: a stream is drained
  rather than polled
why: A reader could not tell a current spike from one describing a
design that has since been removed, and four of them describe types
that are gone: the execution modes, their seam, the metamodel base
class, and the six-command capture.

what:
- Head each with what survived and what did not
- State in the snapshots guide what a capture now costs, and where the
  number comes from
why: HandleTest grew its own parser for the fenced group a capture now
sends, and GroupedTmux was extracted for exactly that a few commits
later. Two readings of tmux's group semantics can disagree, which is
the failure the shared one exists to prevent.

what:
- HandleTest answers through GroupedTmux like the other doubles
why: The second identity probe this replaced compared a pid and a
version; the fence that replaced it compared only the pid. A pid is
reusable, so a different tmux landing on the one just probed would have
answered as the server the rows were being read from — which the form
it replaced would have caught, and which its own contract claimed.

what:
- Fence a capture on pid and version together
- Refuse a double answering as 3.7 on a pid probed as 3.6
- Fold the socket wait into Await, which the last commit missed
- Say in the grouping table that every row pays the same setup, so the
  ratio between them understates what grouping saves
why: The case waits ten seconds for a pane's shell to print, and a
matrix lane shares its machine with the other eight. It failed that way
on the 3.6 lane, which is the same budget problem the integration suite
had one commit earlier.

what:
- Wait thirty seconds, since these cases wait on a shell and on marker
  scanning rather than on tmux alone
why: mode() handed back tmux's own string in a library whose case is
that tmux state is typed, so a caller compared against "tree-mode" by
hand and a typo failed at runtime rather than at compile time.

what:
- PaneMode names the seven modes tmux has, each present from 3.2a
- mode() answers with one, and raises on a name outside the range
- Kotlin's modeOrNull follows
why: waitFor, waitForWithSignalCapacity, signal and drain each took the
channel name again, so a caller repeated it at every call site and a
typo waited on a channel nothing would ever signal. The two traps
tmux's wait-for carries were documented four times over.

what:
- Channel binds the name once and carries signal, drain and the two
  waits, the way options(), hooks() and buffers() already do
- Server keeps channel(name) in place of the four
why: Three methods covered name, content and both, and tmux also takes
a title, a case-insensitive match and a regular expression. Naming
every combination would have meant a method each; what was there was an
incomplete surface rather than a crowded one.

what:
- FindSpec collects what to match and where, the shape CaptureSpec uses
- findWindow takes a spec, a builder, or the text alone
- findWindowByName and findWindowByContent go: name it on the spec
- Every flag exists from 3.2a, so nothing here is version-gated
why: The close test subscribed before tmux had acknowledged the control
client, leaving the waiting thread exposed to an attach race.

what:
- Send one checked command before subscribing
why: A large paste could arrive while the inherited shell was still
starting, so the test observed startup timing instead of paste capacity.

what:
- Start a fixed noncanonical reader pane
- Wait for its readiness marker before pasting
Why: The live tmux test assumed an attached shell would remain silent long enough to prove its subscriber was blocked. JDK 25 exposed legitimate pane output winning that race.

What: Exercise the same client-close wakeup contract against a controlled fake control process that emits no notifications.
why: Caller comments and parentheses could alter completion framing and
escape the subshell that protects the pane's interactive shell.

what:
- Source exact caller text from an owner-only temporary file
- Remove staged text on success, timeout, cancellation, and failure
- Exercise exit, comment, and parenthesis isolation against real tmux
why: A child that stopped reading could trap its caller in a synchronous
write past the request deadline and retain admission forever.

what:
- Pump stdin on a reserved platform worker beside both output drains
- Reclaim the input worker before returning process admission
- Exercise a blocked real child, its deadline, cleanup, and permit reuse
why: A stalled client retained every queued message, while cancellation
and close left queued sends live or unresolved.

what:
- Bound admitted sends to 256 messages and 16 MiB of encoded JSON
- Remove cancelled queued sends and return their admission
- Fail admitted sends on close without starting concurrent delegate sends
why: Temporary command staging was deleted as soon as send-keys reported uncertain delivery, before a busy pane shell necessarily opened the file. An accepted caller command could disappear while the framing still ran.

what:
- pass caller text as one quoted eval operand inside the framing subshell
- gate ambiguous delivery and prove the accepted command still executes
why: A send promoted before the previous caller was notified still
reached the delegate when that callback cancelled it.

what:
- Distinguish promoted sends from ones already started
- Release cancelled promotion admission and continue in order
- Cover callback cancellation before delegate start
@tony
tony force-pushed the improvements-00 branch from aad97c4 to c4aa209 Compare August 30, 2026 13:50
tony added 10 commits August 30, 2026 08:57
why: Destructive tools treated an unprovable caller identity as a
different server and could kill the pane hosting the conversation.

what:
- Track outside, different, self, and unknown caller relationships
- Require both the server PID and socket identity before trusting a pane
- Fail closed on destructive calls when caller identity is uncertain
- Cover the refusal against a real isolated tmux server
why: Retained history size is not a lifetime offset, and forged or stale
cursors could be accepted across history compaction or server replacement.

what:
- Authenticate cursors and bind them to the tmux server process
- Resume from strong trailing-line context instead of history offsets
- Fence screen batches to the pane snapshot and require complete replies
- Cover compaction, forged cursors, replacement servers, and failed reads
why: Java character counts understated UTF-8 command sizes, and newline in
a final format field ended a row before its content was complete.

what:
- Measure encoded batch commands in UTF-8 bytes
- Append an explicit randomized record terminator to row formats
- Parse multiline final and single-field rows through that terminator
- Preserve legacy unframed test transports
why: The run protocol uses POSIX shell syntax and otherwise typed its
plumbing into an arbitrary foreground program without completing.

what:
- Refuse tmux_run unless the pane reports a supported POSIX shell
- Keep rejected payloads out of non-shell panes
- Increase framing and channel nonces from 40 to 128 bits
- State the shell precondition in the tool description
why: The branch fixes must satisfy the repository formatting gate.

what:
- Apply the pinned formatter to the touched Java sources and tests
why: The delivery-uncertainty fixture must satisfy the tmux_run
POSIX-shell precondition while exercising the post-delivery failure.

what:
- Remove the foreground tmux wait gate from the fixture
- Retain proof that the accepted send-keys payload still executes
why: The wire test must not race an interactive shell startup while
proving tmux_run under its POSIX-shell contract.

what:
- Create a dedicated /bin/sh pane for the command tool call
@tony
tony merged commit 22ca876 into master Aug 30, 2026
16 checks passed
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