agentmux_srv/server/
http_open.rs1use super::*;
8
9pub(super) async fn handle_pane_open(
16 State(state): State<AppState>,
17 Json(mut req): Json<crate::backend::rpc_types::CommandPaneOpenData>,
18) -> impl IntoResponse {
19 if let Some(meta) = req.meta.as_mut() {
20 crate::server::browser_owner::strip_srv_only_keys(meta);
21 let checked = crate::server::browser_identity::check_new_tab(&state, meta)
23 .and_then(|()| crate::server::browser_identity::check_agent_may_use(meta));
24 if let Err(e) = checked {
25 return (StatusCode::BAD_REQUEST, Json(json!({ "error": e }))).into_response();
26 }
27 }
28 let mut opened_by_agent = None;
33 let mut opening = None;
34 if let Some(conn) = ssh_connection_of(&req) {
35 let block = req.split_reference_block_id.clone().unwrap_or_default();
36 let agent = match app_api::connections::verified_agent(&state, &block, req.auth.as_ref()) {
37 Ok(a) => a,
38 Err(e) => return (StatusCode::FORBIDDEN, Json(json!({ "error": e }))).into_response(),
39 };
40 let what = format!(
41 "open {} in {}",
42 req.file.as_deref().or(req.cwd.as_deref()).unwrap_or("a pane"),
43 req.view
44 );
45 if let Err(e) = app_api::connections::consent_for_ssh(&state, &block, &agent, &conn, &what).await {
46 return (StatusCode::FORBIDDEN, Json(json!({ "error": e }))).into_response();
47 }
48 opening = Some(crate::backend::remote::helper_consent::begin_agent_open(&conn));
50 opened_by_agent = Some(agent);
51 }
52 match app_api::open_pane(&state, req).await {
53 Ok(result) => {
54 if let Some(agent) = &opened_by_agent {
57 crate::backend::remote::helper_consent::note_agent_pane(&result.block_id, agent);
58 }
59 drop(opening);
60 (StatusCode::OK, Json(json!(result))).into_response()
61 }
62 Err(e) => {
63 let status = if e.starts_with("MISSING_ARG") || e.starts_with("INVALID_VIEW") || e.starts_with("INVALID_ARG") {
66 StatusCode::BAD_REQUEST
67 } else {
68 StatusCode::INTERNAL_SERVER_ERROR
69 };
70 (status, Json(json!({ "error": e }))).into_response()
71 }
72 }
73}
74
75fn ssh_connection_of(req: &crate::backend::rpc_types::CommandPaneOpenData) -> Option<String> {
79 let from_meta = req
80 .meta
81 .as_ref()
82 .and_then(|m| m.get("connection"))
83 .and_then(|v| v.as_str());
84 [req.connection.as_deref(), from_meta]
85 .into_iter()
86 .flatten()
87 .find_map(|c| match crate::backend::remote::ConnTarget::parse(c.trim()) {
88 Ok(t @ crate::backend::remote::ConnTarget::Ssh(_)) => Some(t.name()),
89 _ => None,
90 })
91}
92
93pub(super) async fn handle_agent_open(
102 State(state): State<AppState>,
103 Json(req): Json<crate::backend::rpc_types::CommandAgentOpenData>,
104) -> impl IntoResponse {
105 match app_api::open_agent_impl(&state, req).await {
106 Ok(result) => (StatusCode::OK, Json(json!(result))).into_response(),
107 Err(e) => (open_error_status(&e), Json(json!({ "error": e }))).into_response(),
108 }
109}
110
111fn open_error_status(e: &str) -> StatusCode {
117 if e.starts_with("CLI_INSTALLING") {
118 StatusCode::SERVICE_UNAVAILABLE
119 } else if e.starts_with("AGENT_NOT_FOUND")
120 || e.starts_with("TEMPLATE_NOT_OPENABLE")
121 || e.starts_with("INVALID_PROVIDER")
122 || e.starts_with("CLI_NOT_AVAILABLE")
123 {
124 StatusCode::BAD_REQUEST
125 } else {
126 StatusCode::INTERNAL_SERVER_ERROR
127 }
128}
129
130#[cfg(test)]
131mod connection_tests {
132 use super::*;
133 use crate::backend::rpc_types::CommandPaneOpenData;
134
135 fn cmd(view: &str, connection: Option<&str>, meta: Option<serde_json::Value>) -> CommandPaneOpenData {
136 serde_json::from_value(serde_json::json!({
137 "view": view,
138 "file": "/home/u/a.txt",
139 "connection": connection,
140 "meta": meta,
141 }))
142 .unwrap()
143 }
144
145 #[test]
146 fn the_ssh_host_a_pane_open_names_is_found_wherever_it_is() {
147 assert_eq!(ssh_connection_of(&cmd("editor", Some("user@box"), None)).as_deref(), Some("user@box"));
148 assert_eq!(
149 ssh_connection_of(&cmd("files", None, Some(serde_json::json!({ "connection": "box:2222" })))).as_deref(),
150 Some("box:2222")
151 );
152 assert_eq!(ssh_connection_of(&cmd("editor", Some("local"), None)), None);
153 assert_eq!(ssh_connection_of(&cmd("editor", Some("wsl://Ubuntu"), None)), None);
154 assert_eq!(ssh_connection_of(&cmd("editor", None, None)), None);
155 }
156
157 #[test]
158 fn a_pane_on_a_connection_gets_it_in_its_meta_and_media_is_refused_there() {
159 let meta = app_api::pane::build_pane_meta(&cmd("editor", Some("user@box"), None)).unwrap();
160 assert_eq!(meta.get("connection"), Some(&serde_json::json!("user@box")));
161 assert_eq!(meta.get("file"), Some(&serde_json::json!("/home/u/a.txt")));
162 let local = app_api::pane::build_pane_meta(&cmd("editor", Some("local"), None)).unwrap();
163 assert!(local.get("connection").is_none());
164 let err = app_api::pane::build_pane_meta(&cmd("media", Some("user@box"), None)).unwrap_err();
165 assert!(err.starts_with("INVALID_ARG"), "{err}");
166 }
167}
168
169#[cfg(test)]
170mod open_error_status_tests {
171 use super::*;
172
173 #[test]
174 fn a_cli_that_is_still_installing_is_retryable_not_the_callers_fault() {
175 assert_eq!(
176 open_error_status("CLI_INSTALLING: claude 2.1.285 is still being installed"),
177 StatusCode::SERVICE_UNAVAILABLE
178 );
179 }
180
181 #[test]
182 fn the_callers_own_mistakes_are_400s() {
183 for e in [
184 "AGENT_NOT_FOUND: x",
185 "TEMPLATE_NOT_OPENABLE: x",
186 "INVALID_PROVIDER: x",
187 "CLI_NOT_AVAILABLE: x could not be installed",
188 ] {
189 assert_eq!(open_error_status(e), StatusCode::BAD_REQUEST, "{e}");
190 }
191 }
192
193 #[test]
194 fn anything_else_is_a_server_failure() {
195 assert_eq!(
196 open_error_status("agent.open: meta serialize: boom"),
197 StatusCode::INTERNAL_SERVER_ERROR
198 );
199 }
200}