fix(runtime): tighten mobile control security
This commit is contained in:
@@ -372,6 +372,7 @@ codewhale resume --last # resume the most recent sessi
|
|||||||
codewhale resume <SESSION_ID> # resume a specific session by UUID
|
codewhale resume <SESSION_ID> # resume a specific session by UUID
|
||||||
codewhale fork <SESSION_ID> # fork a saved session into a sibling path
|
codewhale fork <SESSION_ID> # fork a saved session into a sibling path
|
||||||
codewhale serve --http # HTTP/SSE API server
|
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 serve --acp # ACP stdio adapter for Zed/custom agents
|
||||||
codewhale run pr <N> # fetch PR and pre-seed review prompt
|
codewhale run pr <N> # fetch PR and pre-seed review prompt
|
||||||
codewhale mcp list # list configured MCP servers
|
codewhale mcp list # list configured MCP servers
|
||||||
@@ -557,7 +558,7 @@ without recreating skills the user deliberately deleted.
|
|||||||
| [CONFIGURATION.md](docs/CONFIGURATION.md) | Full config reference |
|
| [CONFIGURATION.md](docs/CONFIGURATION.md) | Full config reference |
|
||||||
| [MODES.md](docs/MODES.md) | Plan / Agent / YOLO modes |
|
| [MODES.md](docs/MODES.md) | Plan / Agent / YOLO modes |
|
||||||
| [MCP.md](docs/MCP.md) | Model Context Protocol integration |
|
| [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 |
|
| [INSTALL.md](docs/INSTALL.md) | Platform-specific install guide |
|
||||||
| [DOCKER.md](docs/DOCKER.md) | GHCR image, volumes, and Docker usage |
|
| [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 |
|
| [CNB_MIRROR.md](docs/CNB_MIRROR.md) | CNB mirror and China-friendly install notes |
|
||||||
|
|||||||
+2
-1
@@ -316,6 +316,7 @@ codewhale resume --last # 恢复最近会话
|
|||||||
codewhale resume <SESSION_ID> # 按 UUID 恢复指定会话
|
codewhale resume <SESSION_ID> # 按 UUID 恢复指定会话
|
||||||
codewhale fork <SESSION_ID> # 将已保存会话分叉为兄弟路径
|
codewhale fork <SESSION_ID> # 将已保存会话分叉为兄弟路径
|
||||||
codewhale serve --http # HTTP/SSE API 服务
|
codewhale serve --http # HTTP/SSE API 服务
|
||||||
|
codewhale serve --mobile # 局域网移动端控制页,默认启用 token 保护
|
||||||
codewhale serve --acp # Zed/自定义智能体的 ACP stdio 适配器
|
codewhale serve --acp # Zed/自定义智能体的 ACP stdio 适配器
|
||||||
codewhale run pr <N> # 获取 PR 并预填审查提示
|
codewhale run pr <N> # 获取 PR 并预填审查提示
|
||||||
codewhale mcp list # 列出已配置 MCP 服务器
|
codewhale mcp list # 列出已配置 MCP 服务器
|
||||||
@@ -494,7 +495,7 @@ description: 当 DeepSeek 需要遵循我的自定义工作流时使用这个技
|
|||||||
| [CONFIGURATION.md](docs/CONFIGURATION.md) | 完整配置参考 |
|
| [CONFIGURATION.md](docs/CONFIGURATION.md) | 完整配置参考 |
|
||||||
| [MODES.md](docs/MODES.md) | Plan / Agent / YOLO 模式 |
|
| [MODES.md](docs/MODES.md) | Plan / Agent / YOLO 模式 |
|
||||||
| [MCP.md](docs/MCP.md) | Model Context Protocol 集成 |
|
| [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) | 各平台安装指南 |
|
| [INSTALL.md](docs/INSTALL.md) | 各平台安装指南 |
|
||||||
| [DOCKER.md](docs/DOCKER.md) | GHCR 镜像、volume 和 Docker 用法 |
|
| [DOCKER.md](docs/DOCKER.md) | GHCR 镜像、volume 和 Docker 用法 |
|
||||||
| [CNB_MIRROR.md](docs/CNB_MIRROR.md) | CNB 镜像和中国大陆友好安装说明 |
|
| [CNB_MIRROR.md](docs/CNB_MIRROR.md) | CNB 镜像和中国大陆友好安装说明 |
|
||||||
|
|||||||
+71
-9
@@ -578,9 +578,9 @@ struct ServeArgs {
|
|||||||
/// Start ACP server over stdio for editor clients such as Zed
|
/// Start ACP server over stdio for editor clients such as Zed
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
acp: bool,
|
acp: bool,
|
||||||
/// Bind host for HTTP server (default localhost)
|
/// Bind host for HTTP server (default localhost; --mobile defaults to 0.0.0.0)
|
||||||
#[arg(long, default_value = "127.0.0.1")]
|
#[arg(long)]
|
||||||
host: String,
|
host: Option<String>,
|
||||||
/// Bind port for HTTP server
|
/// Bind port for HTTP server
|
||||||
#[arg(long, default_value_t = 7878)]
|
#[arg(long, default_value_t = 7878)]
|
||||||
port: u16,
|
port: u16,
|
||||||
@@ -602,6 +602,29 @@ struct ServeArgs {
|
|||||||
insecure_no_auth: bool,
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug, Clone)]
|
#[derive(Subcommand, Debug, Clone)]
|
||||||
enum McpCommand {
|
enum McpCommand {
|
||||||
/// List configured MCP servers
|
/// List configured MCP servers
|
||||||
@@ -942,16 +965,17 @@ async fn main() -> Result<()> {
|
|||||||
} else if http_selected {
|
} else if http_selected {
|
||||||
let config = load_config_from_cli(&cli)?;
|
let config = load_config_from_cli(&cli)?;
|
||||||
let cors_origins = resolve_cors_origins(&config, &args.cors_origin);
|
let cors_origins = resolve_cors_origins(&config, &args.cors_origin);
|
||||||
let host = if args.mobile && args.host == "127.0.0.1" {
|
let bind_host = resolve_serve_bind_host(args.mobile, args.host);
|
||||||
"0.0.0.0".to_string()
|
if bind_host.mobile_rebound_to_lan {
|
||||||
} else {
|
println!(
|
||||||
args.host
|
"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(
|
runtime_api::run_http_server(
|
||||||
config,
|
config,
|
||||||
workspace,
|
workspace,
|
||||||
runtime_api::RuntimeApiOptions {
|
runtime_api::RuntimeApiOptions {
|
||||||
host,
|
host: bind_host.host,
|
||||||
port: args.port,
|
port: args.port,
|
||||||
workers: args.workers.clamp(1, 8),
|
workers: args.workers.clamp(1, 8),
|
||||||
cors_origins,
|
cors_origins,
|
||||||
@@ -5581,6 +5605,44 @@ async fn run_exec_agent(
|
|||||||
Ok(())
|
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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod doctor_endpoint_tests {
|
mod doctor_endpoint_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
+142
-21
@@ -554,8 +554,17 @@ async fn require_runtime_token(
|
|||||||
let Some(expected) = state.runtime_token.as_deref() else {
|
let Some(expected) = state.runtime_token.as_deref() else {
|
||||||
return next.run(req).await;
|
return next.run(req).await;
|
||||||
};
|
};
|
||||||
let authorized = req
|
let authorized = request_has_runtime_token(&req, expected);
|
||||||
.headers()
|
|
||||||
|
if authorized {
|
||||||
|
next.run(req).await
|
||||||
|
} else {
|
||||||
|
runtime_token_required_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_has_runtime_token(req: &Request, expected: &str) -> bool {
|
||||||
|
req.headers()
|
||||||
.get(header::AUTHORIZATION)
|
.get(header::AUTHORIZATION)
|
||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.and_then(|raw| raw.strip_prefix("Bearer "))
|
.and_then(|raw| raw.strip_prefix("Bearer "))
|
||||||
@@ -565,34 +574,61 @@ async fn require_runtime_token(
|
|||||||
.get("x-deepseek-runtime-token")
|
.get("x-deepseek-runtime-token")
|
||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.is_some_and(|token| token == expected)
|
.is_some_and(|token| token == expected)
|
||||||
|| token_from_query(req.uri().query()).is_some_and(|token| token == expected);
|
|| token_from_query(req.uri().query()).is_some_and(|token| token == expected)
|
||||||
|
|
||||||
if authorized {
|
|
||||||
next.run(req).await
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
Json(json!({
|
|
||||||
"error": {
|
|
||||||
"message": "runtime API bearer token required",
|
|
||||||
"status": StatusCode::UNAUTHORIZED.as_u16(),
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn token_from_query(query: Option<&str>) -> Option<&str> {
|
fn runtime_token_required_response() -> Response {
|
||||||
|
(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json(json!({
|
||||||
|
"error": {
|
||||||
|
"message": "runtime API bearer token required",
|
||||||
|
"status": StatusCode::UNAUTHORIZED.as_u16(),
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_from_query(query: Option<&str>) -> Option<String> {
|
||||||
query.and_then(|query| {
|
query.and_then(|query| {
|
||||||
query.split('&').find_map(|pair| {
|
query.split('&').find_map(|pair| {
|
||||||
let (key, value) = pair.split_once('=')?;
|
let (key, value) = pair.split_once('=')?;
|
||||||
(key == "token").then_some(value)
|
(key == "token")
|
||||||
|
.then(|| percent_decode_query_component(value))
|
||||||
|
.flatten()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mobile_page(State(state): State<RuntimeApiState>) -> Response {
|
fn percent_decode_query_component(value: &str) -> Option<String> {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
let mut decoded = Vec::with_capacity(bytes.len());
|
||||||
|
let mut index = 0;
|
||||||
|
while index < bytes.len() {
|
||||||
|
match bytes[index] {
|
||||||
|
b'%' => {
|
||||||
|
let hi = *bytes.get(index + 1)?;
|
||||||
|
let lo = *bytes.get(index + 2)?;
|
||||||
|
let hi = (hi as char).to_digit(16)? as u8;
|
||||||
|
let lo = (lo as char).to_digit(16)? as u8;
|
||||||
|
decoded.push((hi << 4) | lo);
|
||||||
|
index += 3;
|
||||||
|
}
|
||||||
|
b'+' => {
|
||||||
|
decoded.push(b' ');
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
byte => {
|
||||||
|
decoded.push(byte);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String::from_utf8(decoded).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn mobile_page(State(state): State<RuntimeApiState>, req: Request) -> Response {
|
||||||
if !state.mobile_enabled {
|
if !state.mobile_enabled {
|
||||||
return (
|
return (
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
@@ -600,6 +636,11 @@ async fn mobile_page(State(state): State<RuntimeApiState>) -> Response {
|
|||||||
)
|
)
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
|
if let Some(expected) = state.runtime_token.as_deref()
|
||||||
|
&& !request_has_runtime_token(&req, expected)
|
||||||
|
{
|
||||||
|
return runtime_token_required_response();
|
||||||
|
}
|
||||||
Html(MOBILE_HTML).into_response()
|
Html(MOBILE_HTML).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2035,6 +2076,15 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_from_query_decodes_percent_encoded_token() {
|
||||||
|
assert_eq!(
|
||||||
|
token_from_query(Some("since_seq=0&token=abc%20ABC%2B%2F%3F%3A%3D%26%25")),
|
||||||
|
Some("abc ABC+/?:=&%".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(token_from_query(Some("token=bad%ZZ")), None);
|
||||||
|
}
|
||||||
|
|
||||||
async fn spawn_test_server_with_root(
|
async fn spawn_test_server_with_root(
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
sessions_dir: PathBuf,
|
sessions_dir: PathBuf,
|
||||||
@@ -3739,6 +3789,77 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mobile_page_requires_runtime_token_when_auth_enabled() -> Result<()> {
|
||||||
|
let tmp = tempfile::tempdir()?;
|
||||||
|
let root = tmp.path().to_path_buf();
|
||||||
|
let sessions_dir = root.join("sessions");
|
||||||
|
let token = "abc ABC+/?:=&%".to_string();
|
||||||
|
let Some((addr, _runtime_threads, handle)) = spawn_test_server_with_root_token_and_mobile(
|
||||||
|
root,
|
||||||
|
sessions_dir,
|
||||||
|
Some(token.clone()),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let unauthorized = client.get(format!("http://{addr}/mobile")).send().await?;
|
||||||
|
assert_eq!(unauthorized.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
|
||||||
|
let encoded = url_query_component(&token);
|
||||||
|
let query = client
|
||||||
|
.get(format!("http://{addr}/mobile?token={encoded}"))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
assert!(query.text().await?.contains("CodeWhale Mobile"));
|
||||||
|
|
||||||
|
let bearer = client
|
||||||
|
.get(format!("http://{addr}/mobile"))
|
||||||
|
.bearer_auth(&token)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
assert!(bearer.text().await?.contains("CodeWhale Mobile"));
|
||||||
|
|
||||||
|
handle.abort();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mobile_insecure_mode_allows_page_and_v1_routes_without_token() -> Result<()> {
|
||||||
|
let tmp = tempfile::tempdir()?;
|
||||||
|
let root = tmp.path().to_path_buf();
|
||||||
|
let sessions_dir = root.join("sessions");
|
||||||
|
let Some((addr, _runtime_threads, handle)) =
|
||||||
|
spawn_test_server_with_root_token_and_mobile(root, sessions_dir, None, true).await?
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let page = client
|
||||||
|
.get(format!("http://{addr}/mobile"))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
assert!(page.text().await?.contains("CodeWhale Mobile"));
|
||||||
|
|
||||||
|
let summary = client
|
||||||
|
.get(format!("http://{addr}/v1/threads/summary"))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
assert_eq!(summary.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
handle.abort();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn decide_approval_404s_when_nothing_pending() -> Result<()> {
|
async fn decide_approval_404s_when_nothing_pending() -> Result<()> {
|
||||||
let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else {
|
let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else {
|
||||||
|
|||||||
+16
-10
@@ -117,7 +117,7 @@ codewhale doctor --json
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
codewhale serve --http [--host 127.0.0.1] [--port 7878] [--workers 2] [--auth-token TOKEN]
|
codewhale serve --http [--host 127.0.0.1] [--port 7878] [--workers 2] [--auth-token TOKEN]
|
||||||
codewhale serve --mobile [--port 7878] [--auth-token TOKEN]
|
codewhale serve --mobile [--host 0.0.0.0] [--port 7878] [--auth-token TOKEN]
|
||||||
```
|
```
|
||||||
|
|
||||||
Defaults: host `127.0.0.1`, port `7878`, 2 workers (clamped 1–8).
|
Defaults: host `127.0.0.1`, port `7878`, 2 workers (clamped 1–8).
|
||||||
@@ -128,8 +128,10 @@ there is no `[app_server]` config section.
|
|||||||
`/v1/*` routes require a bearer token unless `--insecure` is explicitly set.
|
`/v1/*` routes require a bearer token unless `--insecure` is explicitly set.
|
||||||
Pass `--auth-token TOKEN` or set `DEEPSEEK_RUNTIME_TOKEN=TOKEN` before starting
|
Pass `--auth-token TOKEN` or set `DEEPSEEK_RUNTIME_TOKEN=TOKEN` before starting
|
||||||
the server. If neither is set, the process generates a one-time token and prints
|
the server. If neither is set, the process generates a one-time token and prints
|
||||||
it at startup. `/health`, `/mobile`, and `/v1/runtime/info` remain public for
|
it at startup. `/health` and `/v1/runtime/info` remain public for local
|
||||||
local supervision and bootstrap.
|
supervision and bootstrap. `/mobile` returns 404 when mobile mode is disabled;
|
||||||
|
when mobile mode is enabled and auth is enabled, `/mobile` returns 401 unless
|
||||||
|
the request supplies the runtime token.
|
||||||
|
|
||||||
Authenticated clients can provide the token as `Authorization: Bearer TOKEN`,
|
Authenticated clients can provide the token as `Authorization: Bearer TOKEN`,
|
||||||
`X-DeepSeek-Runtime-Token: TOKEN`, or `?token=TOKEN` for EventSource-style
|
`X-DeepSeek-Runtime-Token: TOKEN`, or `?token=TOKEN` for EventSource-style
|
||||||
@@ -139,9 +141,12 @@ clients that cannot set custom headers.
|
|||||||
|
|
||||||
`codewhale serve --mobile` starts the same HTTP/SSE runtime API and serves a
|
`codewhale serve --mobile` starts the same HTTP/SSE runtime API and serves a
|
||||||
phone-friendly control page at `/mobile`. When the bind host is left at the
|
phone-friendly control page at `/mobile`. When the bind host is left at the
|
||||||
default, mobile mode binds to `0.0.0.0` and prints local/LAN URLs. If a runtime
|
default, mobile mode binds to `0.0.0.0`, prints a warning, and prints local/LAN
|
||||||
token is generated or supplied, the printed mobile URL includes it as a query
|
URLs. Pass `--host 127.0.0.1` to keep the mobile page loopback-only. If a
|
||||||
parameter; the page stores it locally and removes it from the address bar.
|
runtime token is generated or supplied, the printed mobile URL includes it as a
|
||||||
|
query parameter; the page stores it locally and removes it from the address bar.
|
||||||
|
The static HTML page contains no secrets, but it is still token-gated when auth
|
||||||
|
is enabled so unauthenticated LAN clients cannot fingerprint the mobile surface.
|
||||||
|
|
||||||
The mobile page can list/create threads, send prompts, follow live SSE events,
|
The mobile page can list/create threads, send prompts, follow live SSE events,
|
||||||
steer or interrupt an active turn, and resolve normal tool approvals through
|
steer or interrupt an active turn, and resolve normal tool approvals through
|
||||||
@@ -331,10 +336,11 @@ Common event names: `thread.started`, `thread.forked`, `turn.started`,
|
|||||||
## Security boundary
|
## Security boundary
|
||||||
|
|
||||||
- **Localhost by default**. The server binds to `127.0.0.1` by default.
|
- **Localhost by default**. The server binds to `127.0.0.1` by default.
|
||||||
`--mobile` binds to `0.0.0.0` when the host is left at the default so phones
|
`--mobile` binds to `0.0.0.0` when no host is supplied so phones on the same
|
||||||
on the same LAN can reach it. Set a non-loopback host only when you trust the
|
LAN can reach it, and the CLI prints a warning for that rebind. Pass
|
||||||
network path or have a reverse-proxy / VPN that authenticates. The runtime
|
`--host 127.0.0.1` for a loopback-only mobile page. Set a non-loopback host
|
||||||
does not provide user isolation or TLS.
|
only when you trust the network path or have a reverse-proxy / VPN that
|
||||||
|
authenticates. The runtime does not provide user isolation or TLS.
|
||||||
- **Optional token guard**. `--auth-token` or `DEEPSEEK_RUNTIME_TOKEN`
|
- **Optional token guard**. `--auth-token` or `DEEPSEEK_RUNTIME_TOKEN`
|
||||||
requires a matching bearer token for `/v1/*` routes. This is a local
|
requires a matching bearer token for `/v1/*` routes. This is a local
|
||||||
convenience guard, not a replacement for TLS, VPN, or a trusted reverse
|
convenience guard, not a replacement for TLS, VPN, or a trusted reverse
|
||||||
|
|||||||
Reference in New Issue
Block a user