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
8 changes: 8 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@
# incremental=true for normal dev; this file overrides it for developers who
# have sccache configured. See docs/RUST.md §Repo-specific overrides.
incremental = false

[profile.dev]
# The all-features labby test harness is large enough that LLVM can exhaust
# memory on this host when the global Cargo config forces debug info on.
debug = 0

[profile.test]
debug = 0
1 change: 1 addition & 0 deletions crates/lab/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ categories = ["command-line-utilities", "development-tools"]
[[bin]]
name = "labby"
path = "src/main.rs"
test = false

[dependencies]
lab-apis = { path = "../lab-apis", default-features = false }
Expand Down
2 changes: 1 addition & 1 deletion crates/lab/src/api/services/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ mod tests {
async fn gateway_test_accepts_proposed_spec() {
let response = post_gateway_fresh(json!({
"action":"gateway.test",
"params":{"spec":{"name":"fixture-stdio","command":"echo","args":["hello"]}}
"params":{"allow_stdio":true,"spec":{"name":"fixture-stdio","command":"echo","args":["hello"]}}
}))
.await;
assert_eq!(response.status(), StatusCode::OK);
Expand Down
126 changes: 125 additions & 1 deletion crates/lab/src/cli/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ use crate::cli::helpers::{run_action_command, run_confirmable_action_command};
use crate::config::{LabConfig, ProtectedMcpRouteConfig, config_toml_path};
use crate::dispatch::clients::SharedServiceClients;
use crate::dispatch::gateway::SHARED_GATEWAY_OAUTH_SUBJECT;
use crate::dispatch::gateway::code_mode::{
CodeModeBroker, CodeModeCaller, CodeModeSurface, CodeModeToolId, CodeModeToolRef,
};
use crate::dispatch::gateway::install_gateway_manager;
use crate::dispatch::gateway::manager::{GatewayManager, GatewayRuntimeHandle};
use crate::dispatch::upstream::pool::UpstreamPool;
Expand Down Expand Up @@ -45,6 +48,33 @@ pub enum GatewayCommand {
Pending(GatewayPendingArgs),
/// Show resolved public URL configuration (app and MCP gateway)
PublicUrls,
/// Search, inspect, and execute Code Mode snippets through dispatch
Code(GatewayCodeArgs),
}

#[derive(Debug, Args)]
pub struct GatewayCodeArgs {
#[command(subcommand)]
pub command: GatewayCodeCommand,
}

#[derive(Debug, Subcommand)]
pub enum GatewayCodeCommand {
/// Search Code Mode tool IDs by natural-language query
Search {
query: String,
#[arg(long, default_value_t = 10)]
top_k: usize,
},
/// Show the schema and generated bindings for one Code Mode tool ID
Schema { id: String },
/// Execute a sandboxed JavaScript snippet that calls callTool(id, params)
Exec {
#[arg(long, conflicts_with = "file")]
code: Option<String>,
#[arg(long)]
file: Option<std::path::PathBuf>,
},
}

#[derive(Debug, Args)]
Expand Down Expand Up @@ -387,7 +417,13 @@ pub async fn run(args: GatewayArgs, format: OutputFormat, config: &LabConfig) ->
command: GatewayMcpAuthCommand::Status(_) | GatewayMcpAuthCommand::Clear(_),
}),
})
) || matches!(&args.command, GatewayCommand::ProtectedRoute(_)));
) || matches!(&args.command, GatewayCommand::ProtectedRoute(_)))
&& !matches!(
&args.command,
GatewayCommand::Code(GatewayCodeArgs {
command: GatewayCodeCommand::Schema { id },
}) if code_mode_schema_is_builtin(id)
);
let manager = build_manager(config, discover_upstreams).await;
let cli_origin = format!("cli:{}", std::process::id());
let cli_owner = json!({
Expand Down Expand Up @@ -506,6 +542,9 @@ pub async fn run(args: GatewayArgs, format: OutputFormat, config: &LabConfig) ->
}
},
command => {
if let GatewayCommand::Code(args) = command {
return run_gateway_code(manager, args, format).await;
}
let mut confirmed = true;
let mut dry_run = false;
let (action, params) = match command {
Expand Down Expand Up @@ -663,6 +702,7 @@ pub async fn run(args: GatewayArgs, format: OutputFormat, config: &LabConfig) ->
},
GatewayCommand::PublicUrls => ("gateway.public_urls.get".to_string(), json!({})),
GatewayCommand::Mcp(_) => unreachable!("handled above"),
GatewayCommand::Code(_) => unreachable!("handled above"),
};

if dry_run {
Expand All @@ -686,6 +726,64 @@ pub async fn run(args: GatewayArgs, format: OutputFormat, config: &LabConfig) ->
}
}

fn code_mode_schema_is_builtin(id: &str) -> bool {
matches!(
CodeModeToolId::parse(id),
Ok(CodeModeToolId {
reference: CodeModeToolRef::LabAction { .. },
..
})
)
}

async fn run_gateway_code(
manager: Arc<GatewayManager>,
args: GatewayCodeArgs,
format: OutputFormat,
) -> Result<ExitCode> {
const CODE_MODE_CLI_MAX_SOURCE_BYTES: u64 = 20 * 1024;

let registry = manager.builtin_service_registry();
let broker = CodeModeBroker::new(&registry, Some(manager.as_ref()));
let caller = CodeModeCaller::TrustedLocal;
let surface = CodeModeSurface::Cli;

match args.command {
GatewayCodeCommand::Search { query, top_k } => {
let candidates = broker.search(&query, top_k, caller, surface).await?;
crate::output::print(&candidates, format)?;
}
GatewayCodeCommand::Schema { id } => {
let schema = broker.schema(&id, caller, surface).await?;
crate::output::print(&schema, format)?;
}
GatewayCodeCommand::Exec { code, file } => {
let code = match (code, file) {
(Some(code), None) => code,
(None, Some(path)) => {
let metadata = std::fs::metadata(&path)?;
if metadata.len() > CODE_MODE_CLI_MAX_SOURCE_BYTES {
anyhow::bail!("Code Mode source file exceeds 20480 bytes");
}
std::fs::read_to_string(path)?
}
_ => anyhow::bail!("provide exactly one of --code or --file"),
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if code.len() as u64 > CODE_MODE_CLI_MAX_SOURCE_BYTES {
anyhow::bail!("Code Mode source exceeds 20480 bytes");
}
let config = manager.code_mode_config().await;
let max_tool_calls = config.max_tool_calls;
let response = broker
.execute(&code, max_tool_calls, caller, surface, config)
.await?;
crate::output::print(&response, format)?;
}
}

Ok(ExitCode::SUCCESS)
}

async fn run_gateway_oauth_start(
manager: Arc<GatewayManager>,
args: GatewayOauthUpstreamArgs,
Expand Down Expand Up @@ -902,5 +1000,31 @@ mod tests {
])
.is_ok()
);
assert!(Cli::try_parse_from(["lab", "gateway", "code", "search", "movie.search"]).is_ok());
assert!(
Cli::try_parse_from([
"lab",
"gateway",
"code",
"schema",
"lab::radarr.movie.search",
])
.is_ok()
);
assert!(
Cli::try_parse_from([
"lab",
"gateway",
"code",
"exec",
"--code",
"await callTool(\"lab::gateway.gateway.servers\", {})",
])
.is_ok()
);
assert!(
Cli::try_parse_from(["lab", "gateway", "code", "exec", "--file", "snippet.js",])
.is_ok()
);
}
}
34 changes: 10 additions & 24 deletions crates/lab/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,14 +539,11 @@ impl CodeModeConfig {
impl ToolSearchConfig {
/// Resolve Qdrant URL: config field → `QDRANT_URL` env var → None.
pub fn resolved_qdrant_url(&self) -> Option<String> {
resolve_container_service_url(
self.qdrant_url
.clone()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("QDRANT_URL").ok().filter(|s| !s.is_empty())),
"axon-qdrant",
6333,
)
self.qdrant_url
.clone()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("QDRANT_URL").ok().filter(|s| !s.is_empty()))
.map(|raw| normalize_container_loopback_url(&raw, "axon-qdrant", 6333))
}

/// Resolve Qdrant API key: config field → `QDRANT_API_KEY` env var → None.
Expand All @@ -564,14 +561,11 @@ impl ToolSearchConfig {

/// Resolve TEI URL: config field → `TEI_URL` env var → None.
pub fn resolved_tei_url(&self) -> Option<String> {
resolve_container_service_url(
self.tei_url
.clone()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("TEI_URL").ok().filter(|s| !s.is_empty())),
"axon-tei",
80,
)
self.tei_url
.clone()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("TEI_URL").ok().filter(|s| !s.is_empty()))
.map(|raw| normalize_container_loopback_url(&raw, "axon-tei", 80))
}

/// Resolve TEI API key: config field → `TEI_API_KEY` env var → None.
Expand Down Expand Up @@ -603,14 +597,6 @@ impl ToolSearchConfig {
}
}

fn resolve_container_service_url(
configured: Option<String>,
docker_host: &str,
docker_port: u16,
) -> Option<String> {
configured.map(|raw| normalize_container_loopback_url(&raw, docker_host, docker_port))
}

fn normalize_container_loopback_url(raw: &str, docker_host: &str, docker_port: u16) -> String {
normalize_container_loopback_url_for_runtime(
raw,
Expand Down
Loading
Loading