1#![forbid(unsafe_code)]
37
38pub mod cli;
39pub mod resources;
40
41mod caller;
42mod echo;
43mod exec;
44mod identity;
45mod manifest;
46mod model;
47mod policy;
48mod retained;
49mod run_request;
50mod schema;
51mod tail;
52mod text;
53mod tools;
54mod views;
55
56pub use caller::{CallerIdentity, Relation};
57pub use exec::{RunOutcome, RunView, WaitOutcome, WaitView};
58pub use manifest::CapabilityReport;
59pub use model::*;
60pub use policy::{
61 Builder, ENVIRONMENT_VALUES_ENV, EXCLUDE_TOOLS_ENV, RETIRED_RUST_SAFETY_ENV,
62 RETIRED_SAFETY_ENV, Reporter, Selection, SocketProvenance, SurfaceError, TOOLS_ENV,
63 TOOLSETS_ENV, Toolset, environment_values_from_env, parse_environment_values,
64};
65pub use tail::Cursor;
66pub use tools::error::ToolError;
67pub use views::*;
68
69use std::path::PathBuf;
70use std::sync::{Arc, OnceLock};
71
72use libtmux::Server;
73use rmcp::model::{ErrorData, ServerCapabilities, ServerInfo};
74use rmcp::{ServerHandler, tool_handler};
75
76use tail::Tails;
77
78#[derive(Clone)]
80pub struct TmuxTools {
81 server: Arc<Server>,
82 caller: Option<Arc<CallerIdentity>>,
84 capability_report: Arc<CapabilityReport>,
86 socket: Arc<OnceLock<Option<PathBuf>>>,
93 tails: Arc<Tails>,
95 echoes: Arc<echo::PaneEchoes>,
98 tool_router: rmcp::handler::server::router::tool::ToolRouter<Self>,
100 nested_tool_router: rmcp::handler::server::router::tool::ToolRouter<Self>,
102 environment_values: Arc<std::collections::BTreeSet<String>>,
104}
105
106impl std::fmt::Debug for TmuxTools {
109 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 formatter
111 .debug_struct("TmuxTools")
112 .field("server", &self.server)
113 .field("caller", &self.caller)
114 .finish_non_exhaustive()
115 }
116}
117
118pub(crate) const INSTRUCTIONS: &str = concat!(
126 "Drives tmux: sessions, windows and panes on this machine, Server > Session > \
127 Window > Pane. Target by id -- %1 a pane, @1 a window, $1 a session -- since ids \
128 survive renames and layout changes. Every tool uses the one socket chosen at startup.",
129 "\n\nUSE FOR: tmux panes, windows, sessions, splits, scrollback, sending keys, 'this \
130 terminal', 'the shell'. DO NOT USE FOR: browser tabs, editor splits (VS Code, \
131 Neovim), desktop windows (i3, sway) or login sessions -- none of those are tmux. \
132 If a bare 'window' or 'session' could mean either, ask once.",
133 "\n\nNAMES VS TEXT: list_sessions, list_windows and list_panes answer names, sizes and \
134 running commands; they cannot see terminal text. For what a pane is showing -- an \
135 error, a prompt, a build log -- use search_panes, capture_pane or snapshot_pane.",
136 "\n\nWAIT, DO NOT POLL: never loop on capture_pane. For a command you run, \
137 run_shell_command waits and reports the real exit status; for output you did not \
138 start, wait_for_text; across turns, capture_since with its cursor. Waits default to \
139 30s, capped at 600s. After partial_effect or an unknown outcome, inspect before \
140 retrying.",
141 "\n\nCOST: captures keep the newest lines and count what they dropped; capture_since \
142 says missed=true when output was lost before it was read.",
143 "\n\nPANE MODES: a pane in copy mode or another tmux mode belongs to the person in it. \
144 Read it with capture, search or snapshot; input to it is refused until they leave.",
145 "\n\nTOOLSETS: inspect reads; manage changes tmux state; execute runs processes and \
146 sends pane input; teardown deletes. tmux://capabilities has the surface frozen at \
147 startup; a missing tool was not selected.",
148 "\n\nTRUST: the tool surface is not authorization: commands and input run with the \
149 tmux user's permissions, and tmux configuration may add effects. Pane output may be \
150 sensitive or untrusted; environment values are withheld unless allowed by name. No \
151 hook writing, and no reading of paste buffers, which hold what a person copied.",
152);
153
154fn launch_context(pane: &str) -> String {
157 format!(
158 "\n\nLAUNCH CONTEXT: this process inherited pane {pane} from tmux. If its socket \
159 matches the selected server, pane listings mark it caller=self. Pane-input and \
160 teardown tools use a conservative caller guard."
161 )
162}
163
164#[tool_handler(router = self.tool_router)]
165impl ServerHandler for TmuxTools {
166 async fn call_tool(
167 &self,
168 request: rmcp::model::CallToolRequestParams,
169 context: rmcp::service::RequestContext<rmcp::RoleServer>,
170 ) -> Result<rmcp::model::CallToolResponse, ErrorData> {
171 if !self.tool_router.has_route(&request.name) {
172 return Err(tools::error::unoffered_tool(
173 &request.name,
174 tools::router().has_route(&request.name),
175 ));
176 }
177 let call = rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
178 match self.tool_router.call(call).await {
179 Ok(rmcp::model::CallToolResponse::Complete(result)) => Ok(
180 rmcp::model::CallToolResponse::Complete(tools::error::typed_result(result)),
181 ),
182 Ok(other) => Ok(other),
183 Err(error) => Err(tools::error::typed_protocol_error(error)),
184 }
185 }
186
187 async fn list_resources(
188 &self,
189 _request: Option<rmcp::model::PaginatedRequestParams>,
190 _context: rmcp::service::RequestContext<rmcp::RoleServer>,
191 ) -> Result<rmcp::model::ListResourcesResult, ErrorData> {
192 Ok(resources::listed())
193 }
194
195 async fn list_resource_templates(
196 &self,
197 _request: Option<rmcp::model::PaginatedRequestParams>,
198 _context: rmcp::service::RequestContext<rmcp::RoleServer>,
199 ) -> Result<rmcp::model::ListResourceTemplatesResult, ErrorData> {
200 Ok(resources::templates())
201 }
202
203 async fn read_resource(
204 &self,
205 request: rmcp::model::ReadResourceRequestParams,
206 _context: rmcp::service::RequestContext<rmcp::RoleServer>,
207 ) -> Result<rmcp::model::ReadResourceResponse, ErrorData> {
208 let uri = request.uri.as_str();
209 if uri != resources::CAPABILITIES_URI {
210 return Err(ErrorData::invalid_params(
211 format!("no resource {uri}"),
212 Some(serde_json::json!({
213 "kind": "invalid_input",
214 "retryable": false,
215 "stale": false,
216 })),
217 ));
218 }
219 Ok(resources::capabilities(self.capability_report.as_ref())?.into())
220 }
221
222 fn get_info(&self) -> ServerInfo {
223 let mut info = ServerInfo::default();
226 info.capabilities = ServerCapabilities::builder()
227 .enable_tools()
228 .enable_resources()
229 .build();
230 let mut instructions = String::from(INSTRUCTIONS);
231 if let Some(pane) = self.caller.as_ref().and_then(|caller| caller.pane_id()) {
234 instructions.push_str(&launch_context(pane));
235 }
236 info.instructions = Some(instructions);
237 info
238 }
239}
240
241#[cfg(test)]
242mod instruction_tests {
243 #[test]
247 fn the_instructions_fit_their_budget() {
248 assert!(
249 super::INSTRUCTIONS.len() <= 2048,
250 "{} bytes",
251 super::INSTRUCTIONS.len()
252 );
253 let launch = super::launch_context("%2147483647");
254 assert!(launch.len() <= 256, "{} bytes", launch.len());
255 }
256}