feat(runtime-api): list the agent CLIs already installed on this computer

GET /v1/agents/external reports Claude, Codex, Gemini CLI, OpenCode, Kimi
Code, Mistral Vibe, Qoder, CodeBuddy and Grok. Each binary is resolved on
PATH and in the usual per-user bin directories, then probed concurrently
with `--version` and, where the CLI speaks Agent Client Protocol itself,
one `initialize` over stdio. The response carries the version, whether ACP
is native or needs a separate adapter, the agent's own auth methods, and
its website. `runnable` stays false: this route reports, it does not start
an agent.

The probes are child processes that never receive Codewhale or provider
credentials: CODEWHALE_*, *_API_KEY, *_AUTH_TOKEN, *_ACCESS_KEY and
*_SECRET_KEY are removed from the child environment. Nothing is stored.
Timeouts are 8 s for the handshake and 5 s for --version, so a missing or
hanging CLI cannot stall the route.

Validation:
- Farm build on Boat (farm-722523): `cargo build --bin codewhale`, exit 0,
  with the exact working-tree copies of the two source files.
- Live route against that binary on the same VM:
  - Real CLIs on the image: claude 2.1.319 (adapter), codex 0.167.0
    (adapter), opencode 1.19.5 (native, agent auth), kimi 2.5.2 (native,
    terminal auth), mistral-vibe 3.1.0 (native, agent auth); gemini,
    qoder, codebuddy and grok reported not_installed.
  - Fixture binaries for all nine: each reported the expected
    adapter/native kind, version and both auth method types.
  - Environment: 32 fixture invocations, zero leaked names (no
    CODEWHALE_HOME, provider *_API_KEY or CODEWHALE_SECRET_SENTINEL).
  - Nothing written under CODEWHALE_HOME.
- `node web/scripts/check-facts.mjs` exit 0 after regenerating the derived
  docs from a clean HEAD export.
- Not run here: clippy in this session (it passed on these exact file
  contents earlier today); hosted CI.
This commit is contained in:
CodeWhale Bot committed 2026-11-08 04:21:53 -08:00
1 parent 0d1ffc1cb2
commit c4a7d694a0
6 files changed
+383 -2

No files matched your search

+6
View File
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Runtime API: find the agent CLIs already installed on this computer.**
`GET /v1/agents/external` reports Claude, Codex, Gemini CLI, OpenCode, Kimi
Code, Mistral Vibe, Qoder, CodeBuddy and Grok, with the version each one
reports and whether it answers Agent Client Protocol itself. The probe runs
`--version` and one `initialize` handshake, gets no Codewhale or provider
credentials, and stores nothing. Read-only: it does not start an agent yet.
- **Runtime API: check a provider key before storing it.**
`POST /v1/providers/{id}/key/check` asks the provider's own `/models`
endpoint once whether it accepts a key, and never stores it. A key the
+5
View File
@@ -116,6 +116,7 @@ mod avatars;
mod computer_display;
mod context;
mod diagnostics;
mod external_agents;
mod git;
mod jobs;
pub mod listener;
@@ -3408,6 +3409,10 @@ pub fn build_router(state: RuntimeApiState) -> Router {
secrets::PROVIDER_KEY_BODY_LIMIT_BYTES,
)),
)
.route(
"/v1/agents/external",
get(external_agents::list_external_agents),
)
.route("/v1/providers", get(list_providers))
.route("/v1/providers/{id}/models", get(list_provider_models))
.route(
@@ -0,0 +1,319 @@
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Duration;
use axum::Json;
use serde_json::{Value, json};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
use tokio::task::JoinSet;
const PROBE_TIMEOUT: Duration = Duration::from_secs(8);
const VERSION_TIMEOUT: Duration = Duration::from_secs(5);
const MAX_PROBE_LINE_BYTES: usize = 256 * 1024;
const MAX_TEXT_CHARS: usize = 80;
const MAX_AUTH_METHODS: usize = 8;
struct Agent {
id: &'static str,
name: &'static str,
binary: &'static str,
acp_args: Option<&'static [&'static str]>,
website: &'static str,
}
const AGENTS: &[Agent] = &[
Agent {
id: "claude",
name: "Claude Agent",
binary: "claude",
acp_args: None,
website: "https://github.com/agentclientprotocol/claude-agent-acp",
},
Agent {
id: "codex",
name: "Codex",
binary: "codex",
acp_args: None,
website: "https://github.com/agentclientprotocol/codex-acp",
},
Agent {
id: "gemini",
name: "Gemini CLI",
binary: "gemini",
acp_args: Some(&["--acp"]),
website: "https://geminicli.com",
},
Agent {
id: "opencode",
name: "OpenCode",
binary: "opencode",
acp_args: Some(&["acp"]),
website: "https://opencode.ai",
},
Agent {
id: "kimi",
name: "Kimi Code",
binary: "kimi",
acp_args: Some(&["acp"]),
website: "https://moonshotai.github.io/kimi-code/",
},
Agent {
id: "mistral-vibe",
name: "Mistral Vibe",
binary: "vibe-acp",
acp_args: Some(&[]),
website: "https://mistral.ai/products/vibe",
},
Agent {
id: "qoder",
name: "Qoder",
binary: "qodercli",
acp_args: Some(&["--acp"]),
website: "https://qoder.com",
},
Agent {
id: "codebuddy",
name: "CodeBuddy",
binary: "codebuddy",
acp_args: Some(&["--acp"]),
website: "https://www.codebuddy.cn/cli/",
},
Agent {
id: "grok",
name: "Grok",
binary: "grok",
acp_args: Some(&["agent", "stdio"]),
website: "https://x.ai/cli",
},
];
const USER_BIN_DIRS: &[&str] = &[
".local/bin",
".npm-global/bin",
".opencode/bin",
".kimi-code/bin",
".bun/bin",
".cargo/bin",
];
const SYSTEM_BIN_DIRS: &[&str] = &["/opt/homebrew/bin", "/usr/local/bin"];
fn resolve(binary: &str) -> Option<PathBuf> {
let mut dirs: Vec<PathBuf> = std::env::var_os("PATH")
.map(|path| std::env::split_paths(&path).collect())
.unwrap_or_default();
if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
dirs.extend(USER_BIN_DIRS.iter().map(|dir| home.join(dir)));
}
dirs.extend(SYSTEM_BIN_DIRS.iter().map(PathBuf::from));
dirs.into_iter()
.map(|dir| dir.join(binary))
.find(|candidate| is_executable(candidate))
}
#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.is_ok_and(|meta| meta.is_file() && meta.permissions().mode() & 0o111 != 0)
}
#[cfg(not(unix))]
fn is_executable(path: &Path) -> bool {
path.is_file()
}
fn foreign_command(path: &Path, args: &[&str]) -> Command {
let mut command = Command::new(path);
command
.args(args)
.current_dir(std::env::temp_dir())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.kill_on_drop(true);
for (name, _) in std::env::vars_os() {
let Some(name) = name.to_str() else { continue };
if name.starts_with("CODEWHALE_")
|| name.ends_with("_API_KEY")
|| name.ends_with("_AUTH_TOKEN")
|| name.ends_with("_ACCESS_KEY")
|| name.ends_with("_SECRET_KEY")
{
command.env_remove(name);
}
}
command
}
fn clean(text: &str) -> String {
text.chars()
.filter(|ch| !ch.is_control())
.take(MAX_TEXT_CHARS)
.collect::<String>()
.trim()
.to_string()
}
fn version_token(text: &str) -> Option<String> {
text.split(|ch: char| ch.is_whitespace() || ch == '(' || ch == ')' || ch == ',')
.map(|token| token.trim_start_matches('v'))
.find(|token| {
token.chars().next().is_some_and(|ch| ch.is_ascii_digit()) && token.contains('.')
})
.map(clean)
}
async fn cli_version(path: &Path) -> Option<String> {
let mut command = foreign_command(path, &["--version"]);
command.stdin(Stdio::null());
let output = tokio::time::timeout(VERSION_TIMEOUT, command.output())
.await
.ok()?
.ok()?;
let text = String::from_utf8_lossy(&output.stdout);
version_token(text.lines().next().unwrap_or_default())
}
async fn acp_initialize(path: &Path, args: &[&str]) -> Option<Value> {
let mut child = foreign_command(path, args).spawn().ok()?;
let mut stdin = child.stdin.take()?;
let stdout = child.stdout.take()?;
let request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": 1,
"clientCapabilities": {
"fs": { "readTextFile": false, "writeTextFile": false },
"terminal": false
},
"clientInfo": {
"name": "codewhale",
"title": "Codewhale",
"version": env!("CARGO_PKG_VERSION")
}
}
});
let mut line = request.to_string();
line.push('\n');
stdin.write_all(line.as_bytes()).await.ok()?;
stdin.flush().await.ok()?;
let mut lines = BufReader::new(stdout).lines();
let reply = tokio::time::timeout(PROBE_TIMEOUT, async {
while let Ok(Some(line)) = lines.next_line().await {
if line.len() > MAX_PROBE_LINE_BYTES {
return None;
}
let Ok(message) = serde_json::from_str::<Value>(&line) else {
continue;
};
if message.get("id") == Some(&json!(1)) {
return message.get("result").cloned();
}
}
None
})
.await
.ok()
.flatten();
drop(stdin);
let _ = child.start_kill();
let _ = child.wait().await;
reply
}
fn auth_methods(result: &Value) -> Vec<Value> {
result
.get("authMethods")
.and_then(Value::as_array)
.map(|methods| {
methods
.iter()
.take(MAX_AUTH_METHODS)
.filter_map(|method| {
let id = clean(method.get("id")?.as_str()?);
let name = method
.get("name")
.and_then(Value::as_str)
.map_or_else(|| id.clone(), clean);
let kind = method
.get("type")
.and_then(Value::as_str)
.map_or("agent", clean_kind);
Some(json!({ "id": id, "name": name, "type": kind }))
})
.collect()
})
.unwrap_or_default()
}
fn clean_kind(kind: &str) -> &'static str {
if kind == "terminal" {
"terminal"
} else {
"agent"
}
}
async fn describe(agent: &'static Agent) -> Value {
let Some(path) = resolve(agent.binary) else {
return json!({
"id": agent.id,
"name": agent.name,
"state": "not_installed",
"version": null,
"acp": null,
"auth_methods": [],
"website": agent.website,
"runnable": false,
});
};
let (version, initialized) = tokio::join!(cli_version(&path), async {
match agent.acp_args {
Some(args) => acp_initialize(&path, args).await,
None => None,
}
});
let acp = match (&initialized, agent.acp_args) {
(Some(_), _) => "native",
(None, Some(_)) => "unreachable",
(None, None) => "adapter",
};
let reported = initialized
.as_ref()
.and_then(|result| result.pointer("/agentInfo/version"))
.and_then(Value::as_str)
.map(clean);
json!({
"id": agent.id,
"name": agent.name,
"state": "installed",
"version": reported.or(version),
"acp": acp,
"auth_methods": initialized.as_ref().map(auth_methods).unwrap_or_default(),
"website": agent.website,
"runnable": false,
})
}
pub(super) async fn list_external_agents() -> Json<Value> {
let mut tasks = JoinSet::new();
for agent in AGENTS {
tasks.spawn(describe(agent));
}
let mut found = Vec::with_capacity(AGENTS.len());
while let Some(result) = tasks.join_next().await {
if let Ok(entry) = result {
found.push(entry);
}
}
found.sort_by_key(|entry| {
AGENTS
.iter()
.position(|agent| entry.get("id").and_then(Value::as_str) == Some(agent.id))
.unwrap_or(usize::MAX)
});
Json(json!({ "agents": found }))
}
+27
View File
@@ -3040,6 +3040,33 @@ other failure to ask (a network error, a timeout, another status) answers
The route applies `PUT`'s credential-writability rules and key-shape limits and
refuses OpenAI Codex. An Engine without this route answers `404`.
### `GET /v1/agents/external` — agent CLIs installed on this computer
```json
// response
{ "agents": [
{ "id": "opencode", "name": "OpenCode", "state": "installed",
"version": "1.19.5", "acp": "native",
"auth_methods": [{ "id": "login", "name": "Log in", "type": "agent" }],
"website": "https://opencode.ai", "runnable": false },
{ "id": "qoder", "name": "Qoder", "state": "not_installed", "version": null,
"acp": null, "auth_methods": [], "website": "https://qoder.com", "runnable": false }
] }
```
Reports which external agent CLIs this computer already has, without starting a
session. It covers Claude, Codex, Gemini CLI, OpenCode, Kimi Code, Mistral Vibe,
Qoder, CodeBuddy and Grok. `state` is `installed` or `not_installed`; a binary is
looked up on `PATH` and in the usual per-user bin directories. For an installed
agent the route runs `--version` and, where the CLI speaks Agent Client Protocol
itself, one `initialize` request on stdin, concurrently. `acp` is `native` when
that handshake answered, `adapter` for CLIs that need a separate adapter
process, and `unreachable` when the handshake did not answer. `auth_methods`
comes from the handshake; `type: "terminal"` means the agent can only be signed
in by running it in a terminal. The probes time out after 8 s (5 s for
`--version`), receive no Codewhale or provider credentials, and leave nothing
stored. `runnable` is `false`: this route reports, it does not start an agent.
### `DELETE /v1/providers/{id}/key` — clear a Codewhale-owned credential
```json
+24
View File
@@ -2333,6 +2333,30 @@ Runtime 的提供商或模型默认值。
可视为“无法验证”,仍可 `PUT` 该 key。该路由遵循与 `PUT` 相同的凭据可写规则和 key 形状限制,并拒绝
OpenAI Codex。没有此路由的 Engine 返回 `404`。
### `GET /v1/agents/external` —— 本机已安装的外部 agent CLI
```json
// 响应
{ "agents": [
{ "id": "opencode", "name": "OpenCode", "state": "installed",
"version": "1.19.5", "acp": "native",
"auth_methods": [{ "id": "login", "name": "Log in", "type": "agent" }],
"website": "https://opencode.ai", "runnable": false },
{ "id": "qoder", "name": "Qoder", "state": "not_installed", "version": null,
"acp": null, "auth_methods": [], "website": "https://qoder.com", "runnable": false }
] }
```
报告本机已有哪些外部 agent CLI,不启动任何会话。覆盖 Claude、Codex、Gemini
CLI、OpenCode、Kimi Code、Mistral Vibe、Qoder、CodeBuddy 和 Grok。`state` 为
`installed` 或 `not_installed`;程序会在 `PATH` 以及常见的用户 bin 目录中查找。
对已安装的 agent,该路由会同时执行 `--version`,并在该 CLI 本身支持 Agent
Client Protocol 时通过 stdin 发送一次 `initialize`。`acp` 为 `native` 表示握手
成功,`adapter` 表示需要单独的适配器进程,`unreachable` 表示握手没有回应。
`auth_methods` 来自握手结果;`type: "terminal"` 表示只能在该 agent 自己的终端里
登录。探测超时为 8 秒(`--version` 为 5 秒),不会收到任何 Codewhale 或服务商
凭据,也不会保存任何内容。`runnable` 为 `false`:该路由只做报告,不启动 agent。
### `DELETE /v1/providers/{id}/key` —— 清除 Codewhale 拥有的凭据
```json
+2 -2
View File
@@ -293,8 +293,8 @@
"sources": [
{
"path": "docs/RUNTIME_API.md",
"sha256": "0973aca9e8293123218532d9fbf6068a7efa103560b9581b6eedf32f4da85974",
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. `--stdio` and `--socket` expose compatibility controls.\nBare `app-server` selects initialized JSONL control over stdio. Local stdio,\nsocket and ACP owners do not bind a hidden TCP listener. Explicit `--http`\nretains HTTP/SSE and compatibility routes on the same Runtime owner.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | Initialized JSONL over stdio | Compatibility controls and explicit tool-free turns through the held manager; no hidden TCP listener or full Codex interaction channel. |\n| `codewhale app-server --socket [--socket-path P]` | RPC text messages over WebSocket on a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | Initialized JSONL over stdio | Same selection as `--stdio`; HTTP requires explicit `--http`. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nBare `app-server` does not open an HTTP port. Selected attached HTTP listeners\nretain their explicit bearer and non-loopback security checks.\n\nInitialized stdio/private-socket controls support `thread/loaded/list` with\n`{cursor?: string | null, limit?: number | null}`. The result is\n`{data: string[], nextCursor: string | null}` from the held manager's loaded\nengine snapshot, including idle and archived engines still in memory. It dLine truncated
"sha256": "65db9be1e8b8c252679b54d264b7551f0591e6f8e184739524efb665ed20b35b",
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. `--stdio` and `--socket` expose compatibility controls.\nBare `app-server` selects initialized JSONL control over stdio. Local stdio,\nsocket and ACP owners do not bind a hidden TCP listener. Explicit `--http`\nretains HTTP/SSE and compatibility routes on the same Runtime owner.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | Initialized JSONL over stdio | Compatibility controls and explicit tool-free turns through the held manager; no hidden TCP listener or full Codex interaction channel. |\n| `codewhale app-server --socket [--socket-path P]` | RPC text messages over WebSocket on a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | Initialized JSONL over stdio | Same selection as `--stdio`; HTTP requires explicit `--http`. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nBare `app-server` does not open an HTTP port. Selected attached HTTP listeners\nretain their explicit bearer and non-loopback security checks.\n\nInitialized stdio/private-socket controls support `thread/loaded/list` with\n`{cursor?: string | null, limit?: number | null}`. The result is\n`{data: string[], nextCursor: string | null}` from the held manager's loaded\nengine snapshot, including idle and archived engines still in memory. It dLine truncated
}
]
},