Skip to content
Merged
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
537 changes: 515 additions & 22 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/lab/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ serde_json = { workspace = true, features = ["preserve_order"] }
anyhow.workspace = true
thiserror.workspace = true
jiff.workspace = true
boa_engine = "0.21.1"

tracing.workspace = true
tracing-subscriber.workspace = true
Expand Down
5 changes: 5 additions & 0 deletions crates/lab/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod gateway;
pub mod health;
pub mod help;
pub mod helpers;
pub mod internal;
pub mod logs;
pub mod marketplace;
#[cfg(feature = "mcpregistry")]
Expand Down Expand Up @@ -100,6 +101,9 @@ pub enum Command {
/// Deploy the local lab release binary to SSH targets.
#[cfg(feature = "deploy")]
Deploy(deploy::DeployArgs),
/// Hidden internal process helpers.
#[command(hide = true)]
Internal(internal::InternalArgs),
// [lab-scaffold: cli-variants]
}

Expand Down Expand Up @@ -127,6 +131,7 @@ pub async fn dispatch(cli: Cli, config: LabConfig) -> Result<ExitCode> {
Command::Stash(args) => stash::run(args, format).await,
#[cfg(feature = "deploy")]
Command::Deploy(args) => dispatch_deploy(args, format, config.deploy.clone()).await,
Command::Internal(args) => internal::run(args),
// [lab-scaffold: cli-dispatch]
}
}
Expand Down
25 changes: 25 additions & 0 deletions crates/lab/src/cli/internal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use std::process::ExitCode;

use anyhow::Result;
use clap::Subcommand;

use crate::dispatch::gateway::code_mode;

#[derive(Debug, clap::Args)]
pub struct InternalArgs {
#[command(subcommand)]
pub command: InternalCommand,
}

#[derive(Debug, Subcommand)]
pub enum InternalCommand {
/// Run the sandboxed Code Mode JavaScript helper process.
#[command(hide = true)]
CodeModeRunner,
}

pub fn run(args: InternalArgs) -> Result<ExitCode> {
match args.command {
InternalCommand::CodeModeRunner => Ok(code_mode::run_code_mode_runner_stdio()),
}
}
107 changes: 107 additions & 0 deletions crates/lab/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ pub struct LabConfig {
/// Gateway-wide tool-search mode for all exposed upstream tools.
#[serde(default)]
pub tool_search: ToolSearchConfig,
/// Gateway-wide Code Mode execution settings.
#[serde(default)]
pub code_mode: CodeModeConfig,
/// Upstream MCP servers to proxy through the gateway.
#[serde(default)]
pub upstream: Vec<UpstreamConfig>,
Expand Down Expand Up @@ -362,6 +365,7 @@ impl LabConfig {

pub fn validate(&self) -> Result<(), ConfigError> {
self.tool_search.validate()?;
self.code_mode.validate()?;
for upstream in &self.upstream {
upstream.validate()?;
}
Expand Down Expand Up @@ -484,6 +488,54 @@ impl Default for ToolSearchConfig {
}
}

fn default_code_mode_timeout_ms() -> u64 {
5_000
}

fn default_code_mode_max_tool_calls() -> usize {
8
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeModeConfig {
/// Enable the constrained Code Mode executor. Discovery and schema lookup
/// can be enabled through `[tool_search]` without enabling execution.
#[serde(default)]
pub enabled: bool,
/// Maximum wall-clock time for one Code Mode execution.
#[serde(default = "default_code_mode_timeout_ms")]
pub timeout_ms: u64,
/// Maximum host-brokered tool calls allowed in one Code Mode execution.
#[serde(default = "default_code_mode_max_tool_calls")]
pub max_tool_calls: usize,
}

impl Default for CodeModeConfig {
fn default() -> Self {
Self {
enabled: false,
timeout_ms: default_code_mode_timeout_ms(),
max_tool_calls: default_code_mode_max_tool_calls(),
}
}
}

impl CodeModeConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
if !(1..=60_000).contains(&self.timeout_ms) {
return Err(ConfigError::InvalidCodeModeTimeout {
value: self.timeout_ms,
});
}
if !(1..=50).contains(&self.max_tool_calls) {
return Err(ConfigError::InvalidCodeModeMaxToolCalls {
value: self.max_tool_calls,
});
}
Ok(())
}
}

impl ToolSearchConfig {
/// Resolve Qdrant URL: config field → `QDRANT_URL` env var → None.
pub fn resolved_qdrant_url(&self) -> Option<String> {
Expand Down Expand Up @@ -879,6 +931,10 @@ pub enum ConfigError {
InvalidToolSearchMaxTools { value: usize },
#[error("gateway tool_search.score_floor_fraction={value} is invalid — expected 0.0..=1.0")]
InvalidToolSearchScoreFloor { value: f32 },
#[error("gateway code_mode.timeout_ms={value} is invalid — expected 1..=60000")]
InvalidCodeModeTimeout { value: u64 },
#[error("gateway code_mode.max_tool_calls={value} is invalid — expected 1..=50")]
InvalidCodeModeMaxToolCalls { value: usize },
#[error("protected MCP route '{name}' has invalid {field}: {value}")]
InvalidProtectedRoute {
name: String,
Expand Down Expand Up @@ -2681,6 +2737,57 @@ url = "https://acme.example.com/mcp"
cfg.validate().expect("root tool_search validates");
}

#[test]
fn code_mode_is_root_level_config_and_disabled_by_default() {
let default_cfg = LabConfig::default();
assert!(!default_cfg.code_mode.enabled);
assert_eq!(default_cfg.code_mode.timeout_ms, 5000);
assert_eq!(default_cfg.code_mode.max_tool_calls, 8);

let cfg = toml::from_str::<LabConfig>(
r#"
[code_mode]
enabled = true
timeout_ms = 2500
max_tool_calls = 3
"#,
)
.expect("root code_mode parses");

assert!(cfg.code_mode.enabled);
assert_eq!(cfg.code_mode.timeout_ms, 2500);
assert_eq!(cfg.code_mode.max_tool_calls, 3);
}

#[test]
fn code_mode_validation_rejects_unbounded_execution_settings() {
let cfg = toml::from_str::<LabConfig>(
r#"
[code_mode]
timeout_ms = 0
max_tool_calls = 8
"#,
)
.expect("code_mode parses");
assert!(matches!(
cfg.validate(),
Err(ConfigError::InvalidCodeModeTimeout { value: 0 })
));

let cfg = toml::from_str::<LabConfig>(
r#"
[code_mode]
timeout_ms = 5000
max_tool_calls = 0
"#,
)
.expect("code_mode parses");
assert!(matches!(
cfg.validate(),
Err(ConfigError::InvalidCodeModeMaxToolCalls { value: 0 })
));
}

#[test]
fn protected_route_legacy_backend_path_folds_into_backend_url() {
let mut cfg = toml::from_str::<LabConfig>(
Expand Down
1 change: 1 addition & 0 deletions crates/lab/src/dispatch/gateway.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod catalog;
mod client;
pub mod code_mode;
pub(crate) mod config;
mod config_mutation;
pub mod discovery;
Expand Down
Loading
Loading