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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ codewhale resume --last # resume the most recent sessi
codewhale resume <SESSION_ID> # resume a specific session by UUID
codewhale fork <SESSION_ID> # fork a saved session into a sibling path
codewhale serve --http # HTTP/SSE API server
codewhale serve --mobile # LAN mobile control page; token-gated by default
codewhale serve --acp # ACP stdio adapter for Zed/custom agents
codewhale run pr <N> # fetch PR and pre-seed review prompt
codewhale mcp list # list configured MCP servers
Expand Down Expand Up @@ -557,7 +558,7 @@ without recreating skills the user deliberately deleted.
| [CONFIGURATION.md](docs/CONFIGURATION.md) | Full config reference |
| [MODES.md](docs/MODES.md) | Plan / Agent / YOLO modes |
| [MCP.md](docs/MCP.md) | Model Context Protocol integration |
| [RUNTIME_API.md](docs/RUNTIME_API.md) | HTTP/SSE API server |
| [RUNTIME_API.md](docs/RUNTIME_API.md) | HTTP/SSE API server and mobile control page |
| [INSTALL.md](docs/INSTALL.md) | Platform-specific install guide |
| [DOCKER.md](docs/DOCKER.md) | GHCR image, volumes, and Docker usage |
| [CNB_MIRROR.md](docs/CNB_MIRROR.md) | CNB mirror and China-friendly install notes |
Expand Down
3 changes: 2 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ codewhale resume --last # 恢复最近会话
codewhale resume <SESSION_ID> # 按 UUID 恢复指定会话
codewhale fork <SESSION_ID> # 将已保存会话分叉为兄弟路径
codewhale serve --http # HTTP/SSE API 服务
codewhale serve --mobile # 局域网移动端控制页,默认启用 token 保护
codewhale serve --acp # Zed/自定义智能体的 ACP stdio 适配器
codewhale run pr <N> # 获取 PR 并预填审查提示
codewhale mcp list # 列出已配置 MCP 服务器
Expand Down Expand Up @@ -494,7 +495,7 @@ description: 当 DeepSeek 需要遵循我的自定义工作流时使用这个技
| [CONFIGURATION.md](docs/CONFIGURATION.md) | 完整配置参考 |
| [MODES.md](docs/MODES.md) | Plan / Agent / YOLO 模式 |
| [MCP.md](docs/MCP.md) | Model Context Protocol 集成 |
| [RUNTIME_API.md](docs/RUNTIME_API.md) | HTTP/SSE API 服务 |
| [RUNTIME_API.md](docs/RUNTIME_API.md) | HTTP/SSE API 服务和移动端控制页 |
| [INSTALL.md](docs/INSTALL.md) | 各平台安装指南 |
| [DOCKER.md](docs/DOCKER.md) | GHCR 镜像、volume 和 Docker 用法 |
| [CNB_MIRROR.md](docs/CNB_MIRROR.md) | CNB 镜像和中国大陆友好安装说明 |
Expand Down
114 changes: 102 additions & 12 deletions crates/tui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -572,12 +572,15 @@ struct ServeArgs {
/// Start runtime HTTP/SSE API server
#[arg(long)]
http: bool,
/// Start runtime HTTP/SSE API server with the built-in mobile control page
#[arg(long)]
mobile: bool,
/// Start ACP server over stdio for editor clients such as Zed
#[arg(long)]
acp: bool,
/// Bind host for HTTP server (default localhost)
#[arg(long, default_value = "127.0.0.1")]
host: String,
/// Bind host for HTTP server (default localhost; --mobile defaults to 0.0.0.0)
#[arg(long)]
host: Option<String>,
/// Bind port for HTTP server
#[arg(long, default_value_t = 7878)]
port: u16,
Expand All @@ -599,6 +602,44 @@ struct ServeArgs {
insecure_no_auth: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct ServeBindHost {
host: String,
mobile_rebound_to_lan: bool,
}

fn resolve_serve_bind_host(mobile: bool, host: Option<String>) -> ServeBindHost {
match (mobile, host) {
(true, None) => ServeBindHost {
host: "0.0.0.0".to_string(),
mobile_rebound_to_lan: true,
},
(_, Some(host)) => ServeBindHost {
host,
mobile_rebound_to_lan: false,
},
(false, None) => ServeBindHost {
host: "127.0.0.1".to_string(),
mobile_rebound_to_lan: false,
},
}
}

fn validate_serve_mode_selection(mcp: bool, http: bool, mobile: bool, acp: bool) -> Result<bool> {
if http && mobile {
bail!("--http and --mobile are mutually exclusive; choose one");
}
let http_selected = http || mobile;
let selected_modes = [mcp, http_selected, acp]
.into_iter()
.filter(|selected| *selected)
.count();
if selected_modes != 1 {
bail!("Choose exactly one server mode: --mcp, --http/--mobile, or --acp");
}
Ok(http_selected)
}

#[derive(Subcommand, Debug, Clone)]
enum McpCommand {
/// List configured MCP servers
Expand Down Expand Up @@ -926,28 +967,30 @@ async fn main() -> Result<()> {
let workspace = cli.workspace.clone().unwrap_or_else(|| {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
});
let selected_modes = [args.mcp, args.http, args.acp]
.into_iter()
.filter(|selected| *selected)
.count();
if selected_modes != 1 {
bail!("Choose exactly one server mode: --mcp, --http, or --acp");
}
let http_selected =
validate_serve_mode_selection(args.mcp, args.http, args.mobile, args.acp)?;
if args.mcp {
mcp_server::run_mcp_server(workspace)
} else if args.http {
} else if http_selected {
let config = load_config_from_cli(&cli)?;
let cors_origins = resolve_cors_origins(&config, &args.cors_origin);
let bind_host = resolve_serve_bind_host(args.mobile, args.host);
if bind_host.mobile_rebound_to_lan {
println!(
"WARNING: --mobile is binding to 0.0.0.0 so LAN devices can reach the mobile control page. Use --host 127.0.0.1 to keep mobile loopback-only."
);
}
runtime_api::run_http_server(
config,
workspace,
runtime_api::RuntimeApiOptions {
host: args.host,
host: bind_host.host,
port: args.port,
workers: args.workers.clamp(1, 8),
cors_origins,
auth_token: args.auth_token,
insecure_no_auth: args.insecure_no_auth,
mobile: args.mobile,
},
)
.await
Expand Down Expand Up @@ -5571,6 +5614,53 @@ async fn run_exec_agent(
Ok(())
}

#[cfg(test)]
mod serve_bind_host_tests {
use super::*;

#[test]
fn http_defaults_to_loopback() {
assert_eq!(
resolve_serve_bind_host(false, None),
ServeBindHost {
host: "127.0.0.1".to_string(),
mobile_rebound_to_lan: false,
}
);
}

#[test]
fn mobile_default_rebinds_to_lan_with_warning_flag() {
assert_eq!(
resolve_serve_bind_host(true, None),
ServeBindHost {
host: "0.0.0.0".to_string(),
mobile_rebound_to_lan: true,
}
);
}

#[test]
fn mobile_respects_explicit_loopback_host() {
assert_eq!(
resolve_serve_bind_host(true, Some("127.0.0.1".to_string())),
ServeBindHost {
host: "127.0.0.1".to_string(),
mobile_rebound_to_lan: false,
}
);
}

#[test]
fn http_and_mobile_are_mutually_exclusive() {
let err = validate_serve_mode_selection(false, true, true, false).unwrap_err();
assert!(
err.to_string()
.contains("--http and --mobile are mutually exclusive")
);
}
}

#[cfg(test)]
mod doctor_endpoint_tests {
use super::*;
Expand Down
Loading
Loading