From 212ebbf1205d1e4c180cec59ed6ecbd9de734ef0 Mon Sep 17 00:00:00 2001 From: naps62-yolo Date: Fri, 29 May 2026 06:40:23 +0000 Subject: [PATCH] fix(maestro): stop embedding web bundle in the registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The registry serves no UI — that is maestro-web's job (connection- orchestration spec §"UI hosting"). The rust-embed of web/dist was legacy from NAP-218, before maestro-web was extracted (NAP-241), and it made the registry (and thus the umbrella maestro binary) fail to compile whenever web/dist was absent — e.g. a fresh cargo install --git after the tracked .gitkeep was dropped. Remove web_assets.rs, the rust-embed dep, and the / + /assets routes; the registry is now JSON/WS API only. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/maestro-registry/Cargo.toml | 1 - crates/maestro-registry/src/client_api.rs | 13 +-- crates/maestro-registry/src/lib.rs | 1 - crates/maestro-registry/src/web_assets.rs | 134 ---------------------- crates/maestro-registry/tests/http_api.rs | 8 +- 5 files changed, 6 insertions(+), 151 deletions(-) delete mode 100644 crates/maestro-registry/src/web_assets.rs diff --git a/crates/maestro-registry/Cargo.toml b/crates/maestro-registry/Cargo.toml index e20669b..887e18b 100644 --- a/crates/maestro-registry/Cargo.toml +++ b/crates/maestro-registry/Cargo.toml @@ -19,7 +19,6 @@ jsonwebtoken.workspace = true maestro-config.workspace = true maestro-protocol.workspace = true rand.workspace = true -rust-embed.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/crates/maestro-registry/src/client_api.rs b/crates/maestro-registry/src/client_api.rs index 23f8e1f..4ce3749 100644 --- a/crates/maestro-registry/src/client_api.rs +++ b/crates/maestro-registry/src/client_api.rs @@ -18,7 +18,6 @@ use uuid::Uuid; use crate::events::events_ws; use crate::jwt::issue_attach_jwt; use crate::state::{Registry, RegistryError, RPC_TIMEOUT}; -use crate::web_assets; /// Shared state plumbed into client-facing handlers. #[derive(Debug, Clone)] @@ -31,9 +30,9 @@ pub struct ClientApiState { /// Mount client-facing routes on the given router. Caller is responsible for /// applying the cookie middleware (we do that in [`crate::server::router`]). pub fn routes(state: ClientApiState) -> Router { + // The registry serves no web UI — that's `maestro-web`'s job (see + // connection-orchestration spec §"UI hosting"). Only the JSON/WS API here. Router::new() - .route("/", get(index)) - .route("/assets/*file", get(asset)) .route("/api/daemons", get(list_daemons)) .route("/api/sessions", get(list_sessions).post(create_session)) .route("/api/sessions/:id", axum::routing::delete(delete_session)) @@ -42,14 +41,6 @@ pub fn routes(state: ClientApiState) -> Router { .with_state(state) } -async fn index() -> Response { - web_assets::serve_index() -} - -async fn asset(Path(file): Path) -> Response { - web_assets::serve_asset(&file) -} - async fn list_daemons(State(s): State) -> Json> { let infos: Vec = s .registry diff --git a/crates/maestro-registry/src/lib.rs b/crates/maestro-registry/src/lib.rs index e4c67e7..6320239 100644 --- a/crates/maestro-registry/src/lib.rs +++ b/crates/maestro-registry/src/lib.rs @@ -21,7 +21,6 @@ pub mod run; pub mod server; pub mod state; pub mod token; -pub mod web_assets; pub use run::run; diff --git a/crates/maestro-registry/src/web_assets.rs b/crates/maestro-registry/src/web_assets.rs deleted file mode 100644 index 490e43f..0000000 --- a/crates/maestro-registry/src/web_assets.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! Embedded static assets for the web client bundle. -//! -//! The Vite build in `web/` writes its output to `web/dist/`; `rust-embed` -//! picks that up at `cargo build` time and embeds the files into the binary. -//! That keeps deploy artifacts to a single executable. -//! -//! When the `web/dist` tree is empty (typical for a fresh checkout without -//! `pnpm build`), the handlers below fall back to a "build the web bundle -//! first" placeholder so the rest of the registry still works for testing. -//! -//! See NAP-218. -use axum::http::{header, StatusCode}; -use axum::response::{IntoResponse, Response}; -use rust_embed::RustEmbed; - -#[derive(RustEmbed)] -#[folder = "../../web/dist/"] -struct WebAssets; - -const FALLBACK_INDEX: &str = "maestro\ -

maestro registry

\ -

Web bundle not built. Run pnpm --dir web install \ - then pnpm --dir web build, then rebuild the registry.

"; - -fn content_type_for(path: &str) -> &'static str { - let ext = path.rsplit('.').next().unwrap_or(""); - match ext { - "html" | "htm" => "text/html; charset=utf-8", - "js" | "mjs" => "text/javascript; charset=utf-8", - "css" => "text/css; charset=utf-8", - "json" => "application/json", - "svg" => "image/svg+xml", - "png" => "image/png", - "ico" => "image/x-icon", - "woff" => "font/woff", - "woff2" => "font/woff2", - "map" => "application/json", - "txt" => "text/plain; charset=utf-8", - _ => "application/octet-stream", - } -} - -fn cache_headers(path: &str) -> &'static str { - // Vite emits hashed filenames under /assets/. Those can be cached - // aggressively. Everything else (index.html, the favicon, etc.) should - // not be cached. - if path.starts_with("assets/") { - "public, max-age=31536000, immutable" - } else { - "no-cache" - } -} - -fn serve(path: &str) -> Option { - let asset = WebAssets::get(path)?; - let body = asset.data.into_owned(); - Some( - ( - [ - (header::CONTENT_TYPE, content_type_for(path)), - (header::CACHE_CONTROL, cache_headers(path)), - ], - body, - ) - .into_response(), - ) -} - -/// Serve `dist/index.html` from the embed; fall back to a placeholder -/// pointing the operator at `pnpm build` when the bundle hasn't been built. -pub fn serve_index() -> Response { - if let Some(resp) = serve("index.html") { - return resp; - } - ( - StatusCode::OK, - [(header::CONTENT_TYPE, "text/html; charset=utf-8")], - FALLBACK_INDEX, - ) - .into_response() -} - -/// Serve a hashed asset under `dist/assets/`. -pub fn serve_asset(file: &str) -> Response { - // Reject anything that tries to escape the `assets/` prefix. `rust-embed` - // already won't return parent-directory paths, but we keep the explicit - // check so the surface is obvious. - if file.contains("..") || file.starts_with('/') { - return StatusCode::NOT_FOUND.into_response(); - } - let path = format!("assets/{file}"); - match serve(&path) { - Some(resp) => resp, - None => StatusCode::NOT_FOUND.into_response(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn content_type_routes_common_extensions() { - assert_eq!(content_type_for("foo.js"), "text/javascript; charset=utf-8"); - assert_eq!(content_type_for("a/b.css"), "text/css; charset=utf-8"); - assert_eq!(content_type_for("x.svg"), "image/svg+xml"); - assert_eq!(content_type_for("index.html"), "text/html; charset=utf-8"); - assert_eq!(content_type_for("noext"), "application/octet-stream"); - } - - #[test] - fn cache_assets_immutable_index_no_cache() { - assert!(cache_headers("assets/index-abc.js").contains("immutable")); - assert_eq!(cache_headers("index.html"), "no-cache"); - } - - #[test] - fn serve_asset_rejects_traversal() { - let resp = serve_asset("../secret"); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[test] - fn serve_index_returns_html() { - let resp = serve_index(); - assert_eq!(resp.status(), StatusCode::OK); - let ct = resp - .headers() - .get(header::CONTENT_TYPE) - .and_then(|v| v.to_str().ok()) - .unwrap_or(""); - assert!(ct.starts_with("text/html")); - } -} diff --git a/crates/maestro-registry/tests/http_api.rs b/crates/maestro-registry/tests/http_api.rs index 45519a0..b231969 100644 --- a/crates/maestro-registry/tests/http_api.rs +++ b/crates/maestro-registry/tests/http_api.rs @@ -666,13 +666,13 @@ async fn events_ws_pushes_snapshot_then_deltas() { } #[tokio::test] -async fn index_serves_placeholder() { +async fn root_serves_no_ui() { + // The registry serves no web UI — that's `maestro-web`'s job (see + // connection-orchestration spec §"UI hosting"). `/` has no route. let h = spawn(Duration::from_secs(5)).await; let client = client_with_cookies(); let resp = client.get(h.base()).send().await.unwrap(); - assert!(resp.status().is_success()); - let body = resp.text().await.unwrap(); - assert!(body.contains("maestro")); + assert_eq!(resp.status(), reqwest::StatusCode::NOT_FOUND); } // ---------------------------------------------------------------------------