huskies: merge 554_story_multi_project_gateway_that_proxies_mcp_calls_to_per_project_docker_containers

This commit is contained in:
dave
2026-04-13 13:02:41 +00:00
parent 5806156af3
commit 69dab063a8
2 changed files with 719 additions and 1 deletions
+47 -1
View File
@@ -13,6 +13,7 @@ pub mod crdt_state;
pub mod crdt_sync;
pub mod crdt_wire;
mod db;
pub mod gateway;
mod http;
mod io;
mod llm;
@@ -53,6 +54,8 @@ struct CliArgs {
agent: bool,
/// Rendezvous WebSocket URL for agent mode (e.g. `ws://host:3001/crdt-sync`).
rendezvous: Option<String>,
/// Whether `--gateway` mode was requested (proxy MCP calls to per-project containers).
gateway: bool,
}
/// Parse CLI arguments into `CliArgs`, or exit early for `--help` / `--version`.
@@ -61,6 +64,7 @@ fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
let mut path: Option<String> = None;
let mut init = false;
let mut agent = false;
let mut gateway = false;
let mut rendezvous: Option<String> = None;
let mut i = 0;
@@ -102,6 +106,9 @@ fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
let val = &a["--rendezvous=".len()..];
rendezvous = Some(val.to_string());
}
"--gateway" => {
gateway = true;
}
"init" => {
init = true;
}
@@ -125,13 +132,14 @@ fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
return Err("agent mode requires --rendezvous <URL>".to_string());
}
Ok(CliArgs { port, path, init, agent, rendezvous })
Ok(CliArgs { port, path, init, agent, rendezvous, gateway })
}
fn print_help() {
println!("huskies [OPTIONS] [PATH]");
println!("huskies init [OPTIONS] [PATH]");
println!("huskies agent --rendezvous <URL> [OPTIONS] [PATH]");
println!("huskies --gateway [OPTIONS] [PATH]");
println!();
println!("Serve a huskies project.");
println!();
@@ -151,6 +159,8 @@ fn print_help() {
println!(" --port <PORT> Port to listen on (default: 3001). Persisted to project.toml.");
println!(" --rendezvous <URL> WebSocket URL of the rendezvous peer (agent mode only).");
println!(" Example: ws://server:3001/crdt-sync");
println!(" --gateway Start in gateway mode. Reads projects.toml from PATH");
println!(" (or cwd) and proxies MCP calls to per-project containers.");
}
/// Resolve the optional positional path argument into an absolute `PathBuf`.
@@ -206,6 +216,7 @@ async fn main() -> Result<(), std::io::Error> {
let is_init = cli.init;
let is_agent = cli.agent;
let is_gateway = cli.gateway;
let agent_rendezvous = cli.rendezvous.clone();
let explicit_path = resolve_path_arg(cli.path.as_deref(), &cwd);
@@ -227,6 +238,17 @@ async fn main() -> Result<(), std::io::Error> {
}
}
// ── Gateway mode: multi-project proxy ──────────────────────────────
//
// When `huskies --gateway` is invoked, skip the normal single-project
// server and instead start a lightweight proxy that routes MCP calls
// to per-project Docker containers based on a projects.toml config.
if is_gateway {
let config_dir = explicit_path.unwrap_or_else(|| cwd.clone());
let config_path = config_dir.join("projects.toml");
return gateway::run(&config_path, port).await;
}
if is_init {
// `huskies init [PATH]` — always scaffold, never search parents.
let init_root = explicit_path.unwrap_or_else(|| cwd.clone());
@@ -949,9 +971,33 @@ name = "coder"
assert_eq!(result.path, None);
assert!(!result.init);
assert!(!result.agent);
assert!(!result.gateway);
assert_eq!(result.rendezvous, None);
}
#[test]
fn parse_gateway_flag() {
let args = vec!["--gateway".to_string()];
let result = parse_cli_args(&args).unwrap();
assert!(result.gateway);
assert!(!result.init);
assert!(!result.agent);
}
#[test]
fn parse_gateway_with_port_and_path() {
let args = vec![
"--gateway".to_string(),
"--port".to_string(),
"5000".to_string(),
"/my/config".to_string(),
];
let result = parse_cli_args(&args).unwrap();
assert!(result.gateway);
assert_eq!(result.port, Some(5000));
assert_eq!(result.path, Some("/my/config".to_string()));
}
#[test]
fn parse_unknown_flag_is_error() {
let args = vec!["--serve".to_string()];