diff --git a/crates/labby-gateway/src/gateway/manager/config_ops.rs b/crates/labby-gateway/src/gateway/manager/config_ops.rs index f7f0d7461..448adbd94 100644 --- a/crates/labby-gateway/src/gateway/manager/config_ops.rs +++ b/crates/labby-gateway/src/gateway/manager/config_ops.rs @@ -1,7 +1,7 @@ //! Config reads/writes for upstream entries: `add`, `batch_add`, `update`, //! `remove`, service env config, and the code-mode config mutation. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use tokio::time::Instant; @@ -98,6 +98,17 @@ impl GatewayManager { self.config.read().await.clone() } + /// Return the configured upstream names without cloning the full gateway config. + pub async fn upstream_names(&self) -> BTreeSet { + self.config + .read() + .await + .upstream + .iter() + .map(|upstream| upstream.name.clone()) + .collect() + } + /// Return the current visibility switches for Labby-owned MCP Apps. pub async fn mcp_apps_config(&self) -> McpAppsConfig { self.config.read().await.mcp_apps @@ -532,6 +543,7 @@ impl GatewayManager { match target { "manager" => cfg.mcp_apps.manager = enabled, "codemode" => cfg.code_mode.mcp_ui_enabled = enabled, + "skill_library" => cfg.mcp_apps.skill_library = enabled, "gateway_status" => cfg.mcp_apps.gateway_status = enabled, "server_logs" => cfg.mcp_apps.server_logs = enabled, "add_server" => cfg.mcp_apps.add_server = enabled, @@ -539,6 +551,7 @@ impl GatewayManager { "all" => { cfg.mcp_apps.manager = enabled; cfg.code_mode.mcp_ui_enabled = enabled; + cfg.mcp_apps.skill_library = enabled; cfg.mcp_apps.gateway_status = enabled; cfg.mcp_apps.server_logs = enabled; cfg.mcp_apps.add_server = enabled; @@ -581,6 +594,7 @@ impl GatewayManager { enabled, manager = current.mcp_apps.manager, code_mode = current.code_mode.mcp_ui_enabled, + skill_library = current.mcp_apps.skill_library, add_server = current.mcp_apps.add_server, server_logs = current.mcp_apps.server_logs, gateway_status = current.mcp_apps.gateway_status, diff --git a/crates/labby-gateway/src/gateway/manager/tests/config_ops.rs b/crates/labby-gateway/src/gateway/manager/tests/config_ops.rs index 6ddf1b83a..bc8521160 100644 --- a/crates/labby-gateway/src/gateway/manager/tests/config_ops.rs +++ b/crates/labby-gateway/src/gateway/manager/tests/config_ops.rs @@ -904,6 +904,7 @@ async fn mcp_app_visibility_setting_persists_notifies_and_skips_pool_rebuild() { let mut initial = GatewayConfig::default(); initial.code_mode.mcp_ui_enabled = true; initial.mcp_apps.manager = true; + initial.mcp_apps.skill_library = true; initial.mcp_apps.gateway_status = true; initial.mcp_apps.server_logs = true; initial.mcp_apps.add_server = true; @@ -922,6 +923,7 @@ async fn mcp_app_visibility_setting_persists_notifies_and_skips_pool_rebuild() { assert!(!updated.mcp_apps.manager); assert!(!updated.code_mode.mcp_ui_enabled); + assert!(!updated.mcp_apps.skill_library); assert!(!updated.mcp_apps.gateway_status); assert!(!updated.mcp_apps.server_logs); assert!(!updated.mcp_apps.add_server); @@ -951,6 +953,7 @@ async fn mcp_app_visibility_setting_persists_notifies_and_skips_pool_rebuild() { let persisted = load_gateway_config(&path).expect("load persisted config"); assert!(!persisted.mcp_apps.manager); assert!(!persisted.code_mode.mcp_ui_enabled); + assert!(!persisted.mcp_apps.skill_library); assert!(!persisted.mcp_apps.gateway_status); assert!(!persisted.mcp_apps.server_logs); assert!(!persisted.mcp_apps.add_server); @@ -961,6 +964,7 @@ async fn mcp_app_visibility_setting_persists_notifies_and_skips_pool_rebuild() { assert!(!restarted.code_mode_app_state().is_enabled()); let restarted_apps = restarted.mcp_apps_config().await; assert!(!restarted_apps.manager); + assert!(!restarted_apps.skill_library); assert!(!restarted_apps.gateway_status); assert!(!restarted_apps.server_logs); assert!(!restarted_apps.add_server); diff --git a/crates/labby-runtime/src/gateway_config.rs b/crates/labby-runtime/src/gateway_config.rs index 3ff030458..8a61fba85 100644 --- a/crates/labby-runtime/src/gateway_config.rs +++ b/crates/labby-runtime/src/gateway_config.rs @@ -107,43 +107,34 @@ fn default_mcp_scopes() -> Vec { /// control tool stays available when its manager UI is disabled. Code Mode keeps /// its existing `CodeModeConfig::mcp_ui_enabled` field for backward-compatible /// config. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct McpAppsConfig { /// Attach MCP App metadata to the always-available `mcp_app` control tool and advertise its UI resources. - /// The control tool itself remains available when this is false. - /// Fresh installs expose the manager UI by default. - #[serde(default = "default_true")] + /// The control tool itself remains available when this is false. Fresh installs keep the UI opt-in. + #[serde(default)] pub manager: bool, + /// Attach the Skill Library UI metadata/resources to the `artifacts` service. + /// The underlying text service remains available when this is false. + #[serde(default)] + pub skill_library: bool, /// Advertise the synthetic Add Server app tool and its UI resources. - /// Fresh installs expose the complete Labby app surface by default. - #[serde(default = "default_true")] + /// Fresh installs keep Labby-owned MCP App UIs opt-in. + #[serde(default)] pub add_server: bool, /// Attach the Server Logs app metadata and advertise its UI resources. - /// Fresh installs expose the complete Labby app surface by default. - #[serde(default = "default_true")] + /// Fresh installs keep Labby-owned MCP App UIs opt-in. + #[serde(default)] pub server_logs: bool, /// Advertise the synthetic Gateway Status app tool and its UI resources. - /// Fresh installs expose the complete Labby app surface by default. - #[serde(default = "default_true")] + /// Fresh installs keep Labby-owned MCP App UIs opt-in. + #[serde(default)] pub gateway_status: bool, /// Advertise the schema-backed Settings app tool and its UI resources. - /// Fresh installs expose the complete Labby app surface by default. - #[serde(default = "default_true")] + /// Fresh installs keep Labby-owned MCP App UIs opt-in. + #[serde(default)] pub settings: bool, } -impl Default for McpAppsConfig { - fn default() -> Self { - Self { - manager: true, - add_server: true, - server_logs: true, - gateway_status: true, - settings: true, - } - } -} - // ─── Code Mode ─────────────────────────────────────────────────────────────── /// Model-facing policy applied to oversized final Code Mode results. @@ -216,8 +207,8 @@ pub struct CodeModeConfig { pub trusted_read_only_tools: Vec, /// Whether the explicit `codemode_ui` MCP App tool and resources are advertised. /// The text-only `codemode` executor remains available when this is false. - /// The inspector is enabled by default so Code Mode has a useful first-run UI. - #[serde(default = "default_true")] + /// The inspector is opt-in on fresh installs, like the other Labby-owned MCP App UIs. + #[serde(default)] pub mcp_ui_enabled: bool, /// Whether Code Mode call traces include redacted/capped tool params. #[serde(default = "default_code_mode_trace_params")] @@ -287,7 +278,7 @@ impl Default for CodeModeConfig { Self { enabled: true, trusted_read_only_tools: Vec::new(), - mcp_ui_enabled: true, + mcp_ui_enabled: false, trace_params: default_code_mode_trace_params(), result_shape_policy: CodeModeResultShapePolicy::Off, timeout_ms: default_code_mode_timeout_ms(), @@ -2397,7 +2388,7 @@ client_secret_env = "SECRET" assert_eq!(cfg, expected); assert!(cfg.enabled); assert!(cfg.trusted_read_only_tools.is_empty()); - assert!(cfg.mcp_ui_enabled); + assert!(!cfg.mcp_ui_enabled); assert!(cfg.trace_params); assert_eq!(cfg.timeout_ms, 30_000); assert_eq!(cfg.token_estimate_divisor, 4); @@ -2458,51 +2449,50 @@ client_secret_env = "SECRET" } #[test] - fn mcp_apps_config_defaults_all_managed_apps_enabled() { + fn mcp_apps_config_defaults_all_managed_apps_disabled() { let cfg: McpAppsConfig = toml::from_str("").unwrap(); assert_eq!(cfg, McpAppsConfig::default()); - assert!(cfg.manager); - assert!(cfg.add_server); - assert!(cfg.server_logs); - assert!(cfg.gateway_status); - assert!(cfg.settings); + assert!(!cfg.manager); + assert!(!cfg.skill_library); + assert!(!cfg.add_server); + assert!(!cfg.server_logs); + assert!(!cfg.gateway_status); + assert!(!cfg.settings); } #[test] fn documented_labby_app_defaults_match_code() { // GATEWAY.md is the operator-facing statement of the Labby-owned app - // surface defaults. Code Mode and every managed MCP App now default - // on, so the doc must not still promise an off-by-default posture. + // surface defaults. Code Mode execution stays on, while every Labby-owned + // MCP App UI is opt-in by default. let doc = include_str!("../../../docs/services/GATEWAY.md"); assert!(CodeModeConfig::default().enabled); - assert!(CodeModeConfig::default().mcp_ui_enabled); + assert!(!CodeModeConfig::default().mcp_ui_enabled); assert_eq!( McpAppsConfig::default(), McpAppsConfig { - manager: true, - add_server: true, - server_logs: true, - gateway_status: true, - settings: true, + manager: false, + skill_library: false, + add_server: false, + server_logs: false, + gateway_status: false, + settings: false, } ); assert!( - !doc.contains("defaults to `false` and must be explicitly enabled"), - "docs/services/GATEWAY.md still documents the retired off-by-default app posture" - ); - assert!( - doc.contains("defaults to `true`") || doc.contains("enabled by default"), - "docs/services/GATEWAY.md must state that Labby-owned app surfaces default on" + doc.contains("default to `false`") || doc.contains("opt-in"), + "docs/services/GATEWAY.md must state that Labby-owned app surfaces default off" ); } #[test] fn mcp_apps_config_supports_independent_visibility_switches() { let cfg: McpAppsConfig = toml::from_str( - "manager = true\nadd_server = false\nserver_logs = true\ngateway_status = false\nsettings = false\n", + "manager = true\nskill_library = true\nadd_server = false\nserver_logs = true\ngateway_status = false\nsettings = false\n", ) .unwrap(); assert!(cfg.manager); + assert!(cfg.skill_library); assert!(!cfg.add_server); assert!(cfg.server_logs); assert!(!cfg.gateway_status); diff --git a/crates/labby/src/app_assets.rs b/crates/labby/src/app_assets.rs index 6c4a7caab..ffa3ee3f0 100644 --- a/crates/labby/src/app_assets.rs +++ b/crates/labby/src/app_assets.rs @@ -44,7 +44,8 @@ pub(crate) const SETTINGS_APP_URI: &str = "ui://lab/settings/editor"; pub(crate) const SETTINGS_APP_SKYBRIDGE_URI: &str = "ui://lab/settings/editor.skybridge"; pub(crate) const SETTINGS_APP_HTML: &str = include_str!("mcp/assets/settings_app.html"); -/// Always-on MCP App manager used to control the other Labby-owned app surfaces. +/// Opt-in MCP App manager UI used to control the other Labby-owned app surfaces. +/// The text-only `mcp_app` control tool remains always available. #[cfg(feature = "gateway")] pub(crate) const MCP_APPS_APP_URI: &str = "ui://lab/apps/manage"; #[cfg(feature = "gateway")] diff --git a/crates/labby/src/app_catalog.rs b/crates/labby/src/app_catalog.rs index 79a283a40..5fb7e8d02 100644 --- a/crates/labby/src/app_catalog.rs +++ b/crates/labby/src/app_catalog.rs @@ -67,7 +67,9 @@ pub(crate) fn enabled_versions( add("server-logs", &SERVER_LOGS_APP_VERSION); } #[cfg(feature = "skills")] - add("skill-library", &SKILL_LIBRARY_APP_VERSION); + if config.skill_library { + add("skill-library", &SKILL_LIBRARY_APP_VERSION); + } #[cfg(feature = "gateway")] { if config.add_server { @@ -85,3 +87,31 @@ pub(crate) fn enabled_versions( } rows } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_visibility_publishes_no_labby_app_revisions() { + assert!(enabled_versions(false, McpAppsConfig::default()).is_empty()); + } + + #[cfg(feature = "skills")] + #[test] + fn skill_library_revision_follows_visibility_switch() { + let mut config = McpAppsConfig::default(); + assert!( + enabled_versions(false, config) + .iter() + .all(|(id, _)| id != "skill-library") + ); + + config.skill_library = true; + assert!( + enabled_versions(false, config) + .iter() + .any(|(id, _)| id == "skill-library") + ); + } +} diff --git a/crates/labby/src/cli/serve.rs b/crates/labby/src/cli/serve.rs index 898c932fa..83790d496 100644 --- a/crates/labby/src/cli/serve.rs +++ b/crates/labby/src/cli/serve.rs @@ -992,10 +992,9 @@ fn resolve_web_ui_auth_disabled( /// Name the Labby-owned app surfaces whose `config.toml` section is absent /// and which therefore run at their on-by-default posture. One INFO line at -/// startup, only when something is inherited, so an install upgraded from a -/// release where Code Mode and the MCP App UIs defaulted off can see why they -/// appeared. A missing file inherits everything; an unreadable one is the -/// loader's error to report. +/// startup, only when something is inherited, so an operator can see which +/// surface defaults were applied. A missing file inherits everything; an +/// unreadable one is the loader's error to report. fn log_inherited_app_surface_defaults(config_path: &Path, config: &LabConfig) { let raw = match std::fs::read_to_string(config_path) { Ok(raw) => raw, @@ -1013,13 +1012,13 @@ fn log_inherited_app_surface_defaults(config_path: &Path, config: &LabConfig) { code_mode_enabled = config.code_mode.enabled, code_mode_ui_enabled = config.code_mode.mcp_ui_enabled, mcp_apps_manager = config.mcp_apps.manager, + mcp_apps_skill_library = config.mcp_apps.skill_library, mcp_apps_add_server = config.mcp_apps.add_server, mcp_apps_server_logs = config.mcp_apps.server_logs, mcp_apps_gateway_status = config.mcp_apps.gateway_status, mcp_apps_settings = config.mcp_apps.settings, - "config.toml declares no [code_mode] or [mcp_apps] section; Code Mode and \ - the Labby-owned MCP App UIs default to enabled — set the switches to \ - false to opt out" + "config.toml inherits app surface defaults: text Code Mode stays enabled, \ + while Labby-owned MCP App UIs default off and must be explicitly enabled" ); } diff --git a/crates/labby/src/config.rs b/crates/labby/src/config.rs index fab0ca6f9..cb1b70ec0 100644 --- a/crates/labby/src/config.rs +++ b/crates/labby/src/config.rs @@ -2147,10 +2147,10 @@ fn load_toml_from_paths(candidates: &[PathBuf]) -> Result { } /// Labby-owned app-surface sections that `raw` (a `config.toml` document) -/// does not declare and therefore inherits at their on-by-default posture: -/// `code_mode` (Code Mode plus its inspector UI) and `mcp_apps` (every -/// Labby-owned MCP App UI). Startup names these once so an install upgraded -/// from a release where they defaulted off sees the change. Unparseable input +/// does not declare and therefore inherits from code defaults. `code_mode` +/// keeps text execution enabled while its inspector UI defaults off; `mcp_apps` +/// keeps every Labby-owned MCP App UI opt-in. Startup names inherited sections +/// once so operators can see which defaults were applied. Unparseable input /// yields nothing; the config loader owns that error. #[must_use] pub fn inherited_app_surface_sections(raw: &str) -> Vec<&'static str> { diff --git a/crates/labby/src/mcp/CLAUDE.md b/crates/labby/src/mcp/CLAUDE.md index 54075b998..56f496b01 100644 --- a/crates/labby/src/mcp/CLAUDE.md +++ b/crates/labby/src/mcp/CLAUDE.md @@ -77,10 +77,9 @@ For normal services, `dispatch//dispatch.rs` owns action routing, catal when an upstream call launches a nested MCP App. `codemode_ui` shares the same execution backend and owns the Code Mode inspector metadata. `mcp_app` is the always-available root-gateway control tool for the manager UI, - inspector, Gateway Status, Server Logs, Add Server, and Settings surfaces; it + inspector, Skill Library, Gateway Status, Server Logs, Add Server, and Settings surfaces; it supports per-app and `all` `status|enable|disable` operations. Its own manager - UI is enabled by default and may be disabled, but the text-only control tool remains - available. App mutations require `lab:admin`, are gateway-scoped, and schedule + UI is opt-in by default, while the text-only control tool remains available. App mutations require `lab:admin`, are gateway-scoped, and schedule coalesced `tools/list_changed` plus `resources/list_changed` notifications after the open tool turn drains. `server_logs` keeps its text/service capability when @@ -220,9 +219,9 @@ Resources are read-only. Do not use them for mutations. - `ui://lab/code-mode/*` — Lab's own Code Mode app resources, served locally from bundled HTML (`read_code_mode_app_resource_impl`). The app descriptors bind only to `codemode_ui`; disabling the app hides that tool and these - resources, and direct reads fail as unknown. All Labby-owned app UIs are - enabled by default; a disabled surface must not remain reachable through a cached URI. -- `ui://lab/mcp-apps/manager` — the default-enabled UI for the always-available `mcp_app` + resources, and direct reads fail as unknown. Labby-owned app UIs are opt-in; + a disabled surface must not remain reachable through a cached URI. +- `ui://lab/apps/manage` — the opt-in UI for the always-available `mcp_app` control tool. Disabling this manager UI strips its tool metadata and resource but does not remove the text-only control tool needed to restore app surfaces. - `ui://lab/gateway/add-server` — the admin-only Add Server app bound to the diff --git a/crates/labby/src/mcp/assets/mcp_apps_app.html b/crates/labby/src/mcp/assets/mcp_apps_app.html index 0b7733c12..239095508 100644 --- a/crates/labby/src/mcp/assets/mcp_apps_app.html +++ b/crates/labby/src/mcp/assets/mcp_apps_app.html @@ -24,7 +24,7 @@