Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion crates/labby-gateway/src/gateway/manager/config_ops.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<String> {
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
Expand Down Expand Up @@ -532,13 +543,15 @@ 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,
"settings" => cfg.mcp_apps.settings = enabled,
"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;
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions crates/labby-gateway/src/gateway/manager/tests/config_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
88 changes: 39 additions & 49 deletions crates/labby-runtime/src/gateway_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,43 +107,34 @@ fn default_mcp_scopes() -> Vec<String> {
/// 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.
Expand Down Expand Up @@ -216,8 +207,8 @@ pub struct CodeModeConfig {
pub trusted_read_only_tools: Vec<String>,
/// 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")]
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion crates/labby/src/app_assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
32 changes: 31 additions & 1 deletion crates/labby/src/app_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")
);
}
}
13 changes: 6 additions & 7 deletions crates/labby/src/cli/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"
);
}

Expand Down
8 changes: 4 additions & 4 deletions crates/labby/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2146,10 +2146,10 @@ fn load_toml_from_paths(candidates: &[PathBuf]) -> Result<LabConfig> {
}

/// 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> {
Expand Down
11 changes: 5 additions & 6 deletions crates/labby/src/mcp/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,9 @@ For normal services, `dispatch/<service>/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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading