feat(app-server,tui): run read-only builtins over the Runtime API (#7277)

* feat(app-server,tui): run read-only builtins over the Runtime API

A remote client can now execute the terminal's read-only diagnostics against a
thread and read the engine's own answer, instead of being told where the
command lives. /system, /tokens and /cost are served; every other name,
including a name no host knows, answers exactly as it did before.

The served set is gated twice — by the terminal host's allowlist and by each
portable handler's declared data needs — so the remote surface cannot widen
without the host widening it first, and nothing is filled in from estimates:
absent telemetry keeps its absence encoding, per-route cost rows stay empty
until they can be attributed, and the cost currency resolves through the same
rule the terminal's display uses.

* fix(app-server): report the effective prompt and keep builtin diagnostics read-only

/system read thread.system_prompt, which is only the client override, so an
ordinary thread printed "(no system prompt)". The route now takes the system
prompt, mode and transcript length from the live engine session snapshot, and
an unreadable snapshot is an error instead of a zero message count.

The settings read used Settings::load(), which can migrate a legacy file
(a write), ran blocking I/O on a Tokio worker and defaulted on a malformed
file. It now runs Settings::load_read_only on the blocking pool and surfaces
failures as 500, as the mobile page does.

The route uses loaded_engine so a diagnostic never boots a cold engine (409
instead), and the name echoed in the 404 is capped at 64 characters.

The route test now starts with a runtime token and asserts 401 without it, 409
for a cold engine, and that the effective prompt is reported. The zh_hans
RUNTIME_API mirror gains the route paragraph, and both languages document the
403 for watch-only tokens.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* chore(web): regenerate agent-docs for the RUNTIME_API route paragraph

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* docs(changelog): credit @gaord's read-only builtins over the Runtime API (#7277)

Adds the CHANGELOG line for POST /v1/threads/{id}/commands/builtin, a
docs/CONTRIBUTORS.md entry under Unreleased and @gaord in
UNRELEASED_CONTRIBUTORS, following the #7261 credit. The agent-docs data
is regenerated with web/scripts/derive-facts.mjs because CONTRIBUTORS.md
is one of its sources. check-contributor-credit passes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore(tui): sync the TUI changelog slice after the #7277 credit line

Generated by scripts/sync-changelog.sh.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(runtime): regenerate the route table and agent docs for the builtin commands route (#7277)

docs/RUNTIME_ROUTES.md is generated from the route registrations in
runtime_api.rs; this pull request adds POST /v1/threads/{id}/commands/builtin.
Regenerated with node web/scripts/check-runtime-routes.mjs --write and
node web/scripts/derive-facts.mjs after merging main, which now carries the
Windows compile fix (a37d7fdd27) that this branch's required check needed.

---------

Co-authored-by: CodeWhale Bot <[email protected]>
Co-authored-by: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-11-11 12:58:06 +08:00
1 parent 34a29a0f2a
commit 1f9ad30c1f
16 files changed
+899 -20

No files matched your search

+1
View File
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Website docs:** documentation pages no longer stack a second "Codewhale documentation" header under the site navigation; the line saying which release the docs describe now sits under the breadcrumb.
- **Website languages:** the Roadmap and Contribute pages now show their titles, introductions and actions in all 18 site languages instead of English outside English and Chinese. The translations have had no native review yet.
- **Responses dialect profiles:** Codex, DeepSeek and Concentrate Responses behavior now comes from one checked dialect table instead of per-provider code, a custom `wire = "responses"` provider can set `responses_profile = "standard"` (the default when omitted), and preview shows the profile each Responses request used. A Responses client whose route cannot be resolved now fails when it is created rather than at its first request ([#7261](https://github.com/codewhale-hq/Codewhale/pull/7261)). Thanks [@SamhandsomeLee](https://github.com/SamhandsomeLee).
- **Read-only builtins over the Runtime API:** `POST /v1/threads/{id}/commands/builtin` runs `/system`, `/tokens` or `/cost` against a thread and returns the same answer the terminal prints, including the effective system prompt the engine sends. Any other command name answers 404, a thread whose engine is not loaded or has no live context estimate answers 409, and the route never starts an engine or writes settings ([#7277](https://github.com/codewhale-hq/Codewhale/pull/7277)). Verified by the app-server route tests and terminal-host tests; no editor client uses it yet. Thanks [@gaord](https://github.com/gaord).
- **Automations:** a run missed while Codewhale was closed now waits on reopen for Run now or Skip instead of catching up silently; each automation can choose "If missed: ask" (default), "run once" or "skip". Tested in the engine only; no HTTP route tests, no desktop client yet, and a Mac asleep longer than the grace window counts as closed.
- **Website maintainer:** a scheduled `web-maintainer` workflow proposes website upkeep as pull requests only: regenerated facts, and three DeepSeek lanes (stale version mentions, moved links, docs drift) that may edit only allowlisted copy files and must pass `npm run check` and the web tests. It is off until the repository variable `WEB_MAINTAINER_ENABLED` is set and a capped `WEB_MAINTAINER_DEEPSEEK_API_KEY` secret exists, and it has never run. The Worker's triage, PR-review, stale, duplicate and curate (Today's Dispatch drafting) cron tasks are removed; the digest stays.
- **Website measurement:** the first-party ingest now accepts a closed website funnel schema while preserving existing runtime and browser batches. Reader confirmations remain self-reports, new website data stays first-party, and no deployment or live conversion gain is established by the source change.
+5
View File
@@ -114,6 +114,7 @@ mod account;
mod auth;
mod automations;
mod avatars;
mod builtin_commands;
mod computer_display;
mod context;
mod diagnostics;
@@ -3284,6 +3285,10 @@ pub fn build_router(state: RuntimeApiState) -> Router {
.route("/v1/skills", get(list_skills))
.route("/v1/commands", get(list_commands))
.route("/v1/threads/{id}/commands", post(run_thread_command))
.route(
"/v1/threads/{id}/commands/builtin",
post(builtin_commands::run_builtin_command),
)
.route(
"/v1/threads/{id}/commands/{operation_key}",
get(get_thread_command).delete(cancel_thread_command),
@@ -0,0 +1,255 @@
//! `POST /v1/threads/{id}/commands/builtin` — execute a terminal built-in
//! command against a thread, remotely.
//!
//! The thread's state is assembled into the engine's
//! [`BuiltinCommandHostSnapshot`] (contract projections included) and handed
//! to the installed command catalog, whose terminal-host implementation maps
//! it into the facet envelope and runs the portable handlers. Which commands
//! this serves is the host's decision (`builtin_execution::SERVED`); a name
//! outside that set answers 404 the same way an unknown name does, so the
//! remote surface can never widen without the host widening it first.
//!
//! Phase 1 notes: executions are not receipt-tracked (the phase-1 set is
//! read-only — every served handler is pure over the snapshot), and the
//! `text_only` flag reports commands that also asked the terminal host for an
//! action, which a remote client cannot perform.
use axum::Json;
use axum::extract::{Path, State};
use codewhale_command_contract::facets::{
DebugCostProjection, DebugRouteCost, DebugSystemPrompt, DebugTokenProjection,
};
use codewhale_command_contract::types::{CommandCurrency, CommandMode};
use codewhale_engine::command_catalog::{self, BuiltinCommandHostSnapshot};
use codewhale_models::SystemPrompt;
use serde::Deserialize;
use super::{ApiError, RuntimeApiState};
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct BuiltinCommandApiRequest {
/// The command name (canonical or alias), with or without the slash.
pub name: String,
/// The argument tail after the command word, if any.
#[serde(default)]
pub args: Option<String>,
}
#[derive(serde::Serialize)]
pub(super) struct BuiltinCommandApiResponse {
pub name: String,
pub text: String,
pub is_error: bool,
/// `false` when the command also asked the terminal host for an action
/// the remote client cannot perform.
pub text_only: bool,
}
pub(super) async fn run_builtin_command(
State(state): State<RuntimeApiState>,
Path(thread_id): Path<String>,
Json(request): Json<BuiltinCommandApiRequest>,
) -> Result<Json<BuiltinCommandApiResponse>, ApiError> {
let catalog = command_catalog::installed()
.ok_or_else(|| ApiError::internal("command catalog is not installed"))?;
// Refuse a name outside the host's remote surface before reading any
// thread state: an unknown command and a known-but-unserved one get the
// same answer, and neither can be distinguished by what the read says
// about the daemon's idle threads.
if !catalog.serves_builtin(&request.name) {
return Err(not_served(&request.name));
}
let thread = state
.runtime_threads
.get_thread(&thread_id)
.await
.map_err(super::map_thread_err)?;
let snapshot = assemble_snapshot(&state, &thread_id, &thread).await?;
match catalog.execute_builtin(&request.name, request.args.as_deref(), &snapshot) {
Some(execution) => Ok(Json(BuiltinCommandApiResponse {
name: execution.name,
text: execution.text,
is_error: execution.is_error,
text_only: execution.text_only,
})),
// The host refused the name: the same answer an unknown command gets.
None => Err(not_served(&request.name)),
}
}
fn not_served(name: &str) -> ApiError {
const MAX_ECHOED_NAME_CHARS: usize = 64;
let shown: String = name.chars().take(MAX_ECHOED_NAME_CHARS).collect();
let ellipsis = if shown.len() < name.len() { "..." } else { "" };
ApiError::not_found(format!(
"no remotely executable builtin command '{shown}{ellipsis}'"
))
}
/// Assemble the host snapshot for one thread from the record, the live
/// engine's context budget, the usage aggregate, and the canonical session
/// document. Every projection field is filled from measured data or left at
/// its honest absence encoding; nothing is estimated into place.
async fn assemble_snapshot(
state: &RuntimeApiState,
thread_id: &str,
thread: &codewhale_engine::runtime_threads::ThreadRecord,
) -> Result<BuiltinCommandHostSnapshot, ApiError> {
// The live engine carries the context budget; an idle daemon thread has
// no honest context estimate, so the served diagnostics refuse rather
// than print zeros.
let engine = state
.runtime_threads
.loaded_engine(thread_id)
.await
.ok_or_else(|| {
ApiError::conflict("this thread's engine is not loaded; send a message first")
})?;
let budget = engine
.get_context_budget()
.await
.map_err(super::map_thread_err)?
.ok_or_else(|| {
ApiError::conflict("this thread has no live context estimate yet; send a message first")
})?;
let usage = state
.runtime_threads
.aggregate_usage_for_thread(thread_id)
.await
.map_err(super::map_thread_err)?;
let parent = &usage.parent;
let children = &usage.routed_children;
let combined = usage.combined();
// The daemon tracks no UI chat history (that is the client's layer), so
// the chat count is a true zero; the model transcript and the effective
// system prompt come from the live engine session.
let session = engine
.get_session_snapshot()
.await
.map_err(|err| ApiError::internal(format!("engine session snapshot unavailable: {err}")))?;
let api_message_count = session.messages.len();
let chat_message_count = 0;
// Currency follows the same rule the TUI's cost display applies, so the
// same config answers the same way on both surfaces: a Simplified-Chinese
// UI reading the default "usd" shows CNY, and every other spelling goes
// through the canonical parser (which accepts rmb and yuan) rather than a
// bare string compare. The CNY column is the provider-published subtotal
// only; a USD-picked currency keeps the authoritative USD numbers.
#[cfg(test)]
let env_ticket = codewhale_runtime::test_support::env_scope_ticket();
let settings = tokio::task::spawn_blocking(move || {
#[cfg(test)]
let _membership = codewhale_runtime::test_support::join_env_scope(env_ticket);
codewhale_runtime::settings::Settings::load_read_only()
})
.await
.map_err(|err| ApiError::internal(format!("settings task failed: {err}")))?
.map_err(|err| ApiError::internal(format!("settings unavailable: {err}")))?;
let locale = codewhale_localization::resolve_locale(&settings.locale);
let currency = match (settings.cost_currency.as_str(), locale.tag()) {
("usd", "zh-Hans") => CommandCurrency::Cny,
_ => match codewhale_config::pricing::CostCurrency::from_setting(&settings.cost_currency) {
Some(codewhale_config::pricing::CostCurrency::Cny) => CommandCurrency::Cny,
_ => CommandCurrency::Usd,
},
};
let currency_amount =
|totals: &codewhale_engine::runtime_threads::manager::UsageTotals| match currency {
CommandCurrency::Cny => totals.cost_cny,
CommandCurrency::Usd => totals.cost_usd,
};
let parent_amount = currency_amount(parent);
let children_amount = currency_amount(children);
let total_amount = currency_amount(&combined);
let (priced_turns, unpriced_turns, unpriced_reasons) = match currency {
CommandCurrency::Cny => (
combined.cny_priced_turns,
combined.cny_unpriced_turns,
&combined.cny_unpriced_reasons,
),
CommandCurrency::Usd => (
combined.priced_turns,
combined.unpriced_turns,
&combined.unpriced_reasons,
),
};
let mode = CommandMode::parse(&session.mode).unwrap_or(CommandMode::Agent);
let mode_label = codewhale_config::AppMode::from_setting(&session.mode)
.label()
.to_string();
let system_prompt = match session.system_prompt {
None => DebugSystemPrompt::None,
Some(SystemPrompt::Text(text)) => DebugSystemPrompt::Text(text),
Some(SystemPrompt::Blocks(blocks)) => {
DebugSystemPrompt::Blocks(blocks.into_iter().map(|block| block.text).collect())
}
};
let cost = DebugCostProjection {
currency,
// The combined subtotal is the authoritative total, and it is by
// construction the parent and children amounts added up; the
// terminal's #244 reconciliation floor only exists because its
// display total and its recomputed components can drift apart. The
// remote aggregate cannot drift from itself, so there is no floor to
// report and none is invented.
total: total_amount,
parent_turns: parent_amount,
subagents: children_amount,
display_floor: 0.0,
priced_turns: priced_turns.clamp(0, u32::MAX as u64) as u32,
unpriced_turns: unpriced_turns.clamp(0, u32::MAX as u64) as u32,
legacy_coverage_unknown: !combined.cost_complete,
// Pricing-provenance bookkeeping (user-declared estimates, live
// defects, route receipts) is terminal-session state the daemon
// does not track; the honest encoding is "none recorded".
user_declared_estimates: false,
// Per-route attribution needs per-turn provider identities, which
// the remote snapshot does not assemble yet; the coverage counts
// above stay authoritative. With no route rows, no turn is itemized:
// claiming otherwise would make the renderer state a breakdown it
// cannot show.
itemized_turns: 0,
route_amounts: Vec::<DebugRouteCost>::new(),
turn_history_capacity: 0,
unpriced_reason_labels: unpriced_reasons.iter().cloned().collect(),
unpriced_classes: combined.unpriced_classes.iter().cloned().collect(),
pricing_provenances: Vec::new(),
live_pricing_defects: Vec::new(),
unusable_pricing_defects: Vec::new(),
route_receipts: Vec::new(),
};
Ok(BuiltinCommandHostSnapshot {
mode,
mode_label,
locale,
model: thread.model.clone(),
system_prompt,
tokens: DebugTokenProjection {
active_context_used: (budget.input_tokens as usize).min(budget.window_tokens as usize),
context_window: budget.window_tokens.clamp(0, u32::MAX as u64) as u32,
// The context budget carries no per-turn telemetry, so the
// last-turn columns take their honest absence encoding rather
// than a stale or fabricated value.
last_input: None,
last_output: None,
cache_hit: None,
cache_miss: None,
total_tokens: combined.input_tokens.saturating_add(combined.output_tokens),
cache_write_tokens: combined.cache_write_tokens,
api_message_count,
chat_message_count,
model: thread.model.clone(),
cost,
},
})
}
@@ -828,3 +828,149 @@ async fn turn_detail(
.and_then(|turns| turns.iter().find(|turn| turn["id"] == turn_id))
.map_or_else(|| "turn absent".to_string(), Value::to_string))
}
// ── Remote built-in execution ──
/// A catalog that serves exactly one built-in remotely, so the route's name
/// gate and snapshot hand-off can be observed without the terminal host.
struct RemoteBuiltinCatalog;
impl CommandCatalog for RemoteBuiltinCatalog {
fn answers_to(&self, name: &str) -> bool {
name == "system"
}
fn builtins(&self) -> Vec<BuiltinCommand> {
vec![builtin("system", vec!["xitong".to_string()])]
}
fn user_commands(&self, _workspace: &Path) -> Vec<UserCommand> {
Vec::new()
}
fn serves_builtin(&self, name: &str) -> bool {
matches!(name.trim_start_matches('/'), "system" | "xitong")
}
fn execute_builtin(
&self,
name: &str,
_args: Option<&str>,
host: &codewhale_engine::command_catalog::BuiltinCommandHostSnapshot,
) -> Option<codewhale_engine::command_catalog::BuiltinCommandExecution> {
if !self.serves_builtin(name) {
return None;
}
Some(codewhale_engine::command_catalog::BuiltinCommandExecution {
name: "system".to_string(),
text: format!(
"{}|{}|{}",
host.mode_label,
host.model,
match &host.system_prompt {
codewhale_command_contract::facets::DebugSystemPrompt::None => 0,
codewhale_command_contract::facets::DebugSystemPrompt::Text(text) => text.len(),
codewhale_command_contract::facets::DebugSystemPrompt::Blocks(blocks) =>
blocks.iter().map(String::len).sum(),
}
),
is_error: false,
text_only: true,
})
}
}
/// B5: `POST /v1/threads/{id}/commands/builtin` executes only the names the
/// installed host serves, and answers everything else like an unknown name.
#[tokio::test]
async fn post_thread_builtin_command_serves_only_the_host_allowlist() -> Result<()> {
let _env = lock_test_env();
let temp = tempfile::tempdir()?;
let token = "builtin-route-fixture-token";
let root = temp.path().join("builtin-commands-route");
let sessions_dir = root.join("sessions");
let workspace = root.join("workspace");
fs::create_dir_all(&workspace)?;
let _catalog =
codewhale_engine::command_catalog::CatalogGuard::install(Arc::new(RemoteBuiltinCatalog));
let Some((addr, runtime_threads, handle)) = spawn_test_server_with_root_token_mobile_workspace(
root,
sessions_dir,
Some(token.to_string()),
false,
workspace,
)
.await?
else {
return Ok(());
};
let client = codewhale_release::tls::reqwest_client();
let thread = runtime_threads
.create_thread(CreateThreadRequest::default())
.await?;
let url = format!("http://{addr}/v1/threads/{}/commands/builtin", thread.id);
let unauthorized = client
.post(&url)
.json(&json!({ "name": "system" }))
.send()
.await?;
assert_eq!(unauthorized.status(), StatusCode::UNAUTHORIZED);
let cold = client
.post(&url)
.bearer_auth(token)
.json(&json!({ "name": "system" }))
.send()
.await?;
assert_eq!(cold.status(), StatusCode::CONFLICT, "cold engine");
runtime_threads.get_engine(&thread.id).await?;
// Not on the host's remote surface: the same answer an unknown name gets.
for name in ["balance", "cache", "not-a-command"] {
let response = client
.post(&url)
.bearer_auth(token)
.json(&json!({ "name": name }))
.send()
.await?;
assert_eq!(response.status(), StatusCode::NOT_FOUND, "{name}");
}
// A served name (alias spelling included) reaches the host with the
// thread's own state, and its text comes back unchanged.
for name in ["system", "/system", "xitong"] {
let body: Value = client
.post(&url)
.bearer_auth(token)
.json(&json!({ "name": name, "args": "ignored" }))
.send()
.await?
.error_for_status()?
.json()
.await?;
assert_eq!(body["name"], "system");
assert_eq!(body["is_error"], false);
assert_eq!(body["text_only"], true);
// One snapshot field per segment, so a route that assembled the
// wrong thread's state cannot pass.
let text = body["text"].as_str().context("builtin text")?;
let fields: Vec<&str> = text.split('|').collect();
assert_eq!(fields.len(), 3, "{text}");
assert!(!fields[0].is_empty(), "mode label: {text}");
assert_eq!(fields[1], thread.model, "model: {text}");
let prompt_len: usize = fields[2].parse().context("system prompt length")?;
assert!(prompt_len > 0, "effective system prompt reported: {text}");
}
// The thread id is still the address: a missing thread stays 404.
let missing = client
.post(format!(
"http://{addr}/v1/threads/thr_missing/commands/builtin"
))
.bearer_auth(token)
.json(&json!({ "name": "system" }))
.send()
.await?;
assert_eq!(missing.status(), StatusCode::NOT_FOUND);
handle.abort();
Ok(())
}
@@ -635,7 +635,7 @@ async fn snapshot_in_owner(
snapshot_in_runtime(&state.runtime_threads, &state.sessions_dir, id).await
}
async fn snapshot_in_runtime(
pub(super) async fn snapshot_in_runtime(
runtime: &Arc<RuntimeThreadManager>,
sessions_dir: &Path,
id: String,
+54
View File
@@ -41,6 +41,60 @@ pub trait CommandCatalog: Send + Sync {
fn answers_to(&self, name: &str) -> bool;
fn builtins(&self) -> Vec<BuiltinCommand>;
fn user_commands(&self, workspace: &Path) -> Vec<UserCommand>;
/// Whether this host executes `name` remotely at all. Checked before any
/// thread state is assembled, so a name outside the remote surface is
/// refused exactly like a name no host knows. `false` is the default: a
/// host opts in one command at a time, and widening this must widen
/// [`Self::execute_builtin`] too.
fn serves_builtin(&self, name: &str) -> bool {
let _ = name;
false
}
/// Execute one built-in command against a host snapshot. `None` means
/// this host does not implement remote execution, or does not answer to
/// the name; `Some` carries the command's own answer.
fn execute_builtin(
&self,
name: &str,
args: Option<&str>,
host: &BuiltinCommandHostSnapshot,
) -> Option<BuiltinCommandExecution> {
let _ = (name, args, host);
None
}
}
/// The thread state a remote built-in execution reads.
///
/// Engine-side so the app-server can assemble it from thread state and the
/// terminal host can interpret it without either depending on the other. The
/// projections are the command contract's own data-only types: the host maps
/// them into the facet envelope, and the capability mask each portable
/// handler declares decides which commands this snapshot can honestly serve.
pub struct BuiltinCommandHostSnapshot {
pub mode: codewhale_command_contract::types::CommandMode,
/// The host's own label for `mode` ("WORK", "PLAN"), the same string the
/// terminal footer shows.
pub mode_label: String,
pub locale: codewhale_localization::Locale,
pub model: String,
pub system_prompt: codewhale_command_contract::facets::DebugSystemPrompt,
/// Published usage telemetry and the host's cost breakdown for the
/// thread — the complete data source the portable `/tokens` and `/cost`
/// renderers consume.
pub tokens: codewhale_command_contract::facets::DebugTokenProjection,
}
/// One remote built-in execution's outcome.
pub struct BuiltinCommandExecution {
pub name: String,
pub text: String,
pub is_error: bool,
/// `false` when the command also asked the terminal host for an action —
/// the text is still what the command answered, but no API client can
/// perform that action (a balance fetch, a snapshot restore) on the
/// host's behalf.
pub text_only: bool,
}
static CATALOG: OnceLock<Arc<dyn CommandCatalog>> = OnceLock::new();
+1
View File
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Website docs:** documentation pages no longer stack a second "Codewhale documentation" header under the site navigation; the line saying which release the docs describe now sits under the breadcrumb.
- **Website languages:** the Roadmap and Contribute pages now show their titles, introductions and actions in all 18 site languages instead of English outside English and Chinese. The translations have had no native review yet.
- **Responses dialect profiles:** Codex, DeepSeek and Concentrate Responses behavior now comes from one checked dialect table instead of per-provider code, a custom `wire = "responses"` provider can set `responses_profile = "standard"` (the default when omitted), and preview shows the profile each Responses request used. A Responses client whose route cannot be resolved now fails when it is created rather than at its first request ([#7261](https://github.com/codewhale-hq/Codewhale/pull/7261)). Thanks [@SamhandsomeLee](https://github.com/SamhandsomeLee).
- **Read-only builtins over the Runtime API:** `POST /v1/threads/{id}/commands/builtin` runs `/system`, `/tokens` or `/cost` against a thread and returns the same answer the terminal prints, including the effective system prompt the engine sends. Any other command name answers 404, a thread whose engine is not loaded or has no live context estimate answers 409, and the route never starts an engine or writes settings ([#7277](https://github.com/codewhale-hq/Codewhale/pull/7277)). Verified by the app-server route tests and terminal-host tests; no editor client uses it yet. Thanks [@gaord](https://github.com/gaord).
- **Automations:** a run missed while Codewhale was closed now waits on reopen for Run now or Skip instead of catching up silently; each automation can choose "If missed: ask" (default), "run once" or "skip". Tested in the engine only; no HTTP route tests, no desktop client yet, and a Mac asleep longer than the grace window counts as closed.
- **Website maintainer:** a scheduled `web-maintainer` workflow proposes website upkeep as pull requests only: regenerated facts, and three DeepSeek lanes (stale version mentions, moved links, docs drift) that may edit only allowlisted copy files and must pass `npm run check` and the web tests. It is off until the repository variable `WEB_MAINTAINER_ENABLED` is set and a capped `WEB_MAINTAINER_DEEPSEEK_API_KEY` secret exists, and it has never run. The Worker's triage, PR-review, stale, duplicate and curate (Today's Dispatch drafting) cron tasks are removed; the digest stays.
- **Website measurement:** the first-party ingest now accepts a closed website funnel schema while preserving existing runtime and browser batches. Reader confirmations remain self-reports, new website data stays first-party, and no deployment or live conversion gain is established by the source change.
@@ -0,0 +1,353 @@
//! Remote built-in command execution over the engine's host snapshot.
//!
//! The app-server assembles a [`BuiltinCommandHostSnapshot`] from thread
//! state and calls [`execute_builtin`] through the installed catalog. This
//! module is the terminal host's side: it maps the snapshot into the command
//! contract's facet envelope and runs the portable debug handlers.
//!
//! Phase 1 serves exactly the read-only diagnostics commands whose every
//! data path the snapshot backs faithfully (`/system`, `/tokens`, `/cost`).
//! Two gates keep the surface honest: the [`SERVED`] allowlist, and the
//! handler-declared capability mask checked against [`PROVIDED`]. Commands
//! outside both are refused by returning `None`, which the API renders as
//! an unknown command rather than a degraded answer.
use codewhale_command_contract::facets::{
CommandDebugDiagnosticsContext, CommandPresentationContext, DebugBalanceProjection,
DebugCacheCall, DebugCacheInspectionUnavailable, DebugCacheRates, DebugCacheTelemetry,
DebugCostProjection, DebugPromptContext, DebugPromptSourceMap, DebugSystemProjection,
DebugTokenProjection, DebugToolSnapshot,
};
use codewhale_command_contract::handler::{CommandCapabilities, CommandContexts, CommandHandler};
use codewhale_engine::command_catalog::{BuiltinCommandExecution, BuiltinCommandHostSnapshot};
use codewhale_localization::{Locale, tr};
use crate::commands::contract::{apply_named_replacements, resolve_message_id};
use codewhale_commands::debug as portable_debug;
/// Facets this remote host can honestly provide from the snapshot.
const PROVIDED: CommandCapabilities =
CommandCapabilities::DEBUG_DIAGNOSTICS.union(CommandCapabilities::PRESENTATION);
/// The phase-1 served set: read-only diagnostics whose every projection the
/// snapshot carries. Widening this list requires widening the snapshot with
/// faithfully assembled data first — never an adapter that guesses.
const SERVED: [&str; 3] = ["system", "tokens", "cost"];
// ---------------------------------------------------------------------------
// Facet adapters over the snapshot
// ---------------------------------------------------------------------------
struct SnapshotDiagnostics<'a> {
snapshot: &'a BuiltinCommandHostSnapshot,
}
impl CommandDebugDiagnosticsContext for SnapshotDiagnostics<'_> {
/// Never served remotely: the provider-balance probe is a host action,
/// and the allowlist keeps `/balance` off this surface. The projection
/// states what the snapshot genuinely knows: nothing.
fn balance_projection(&self) -> DebugBalanceProjection {
DebugBalanceProjection {
provider_display_name: "unavailable".to_string(),
supports_balance_api: false,
}
}
fn system_projection(&self) -> DebugSystemProjection {
DebugSystemProjection {
mode_label: self.snapshot.mode_label.clone(),
prompt: self.snapshot.system_prompt.clone(),
}
}
fn token_projection(&self) -> DebugTokenProjection {
self.snapshot.tokens.clone()
}
fn cost_projection(&self) -> DebugCostProjection {
self.snapshot.tokens.cost.clone()
}
/// Assembled prompt-layer telemetry is a terminal-host report; the
/// remote surface does not serve `/context`, so this stays an explicitly
/// empty map rather than a partial reconstruction.
fn context_source_map(&self) -> DebugPromptSourceMap {
DebugPromptSourceMap {
entries: Vec::new(),
total_estimated_tokens: 0,
active_context_estimated_tokens: 0,
overflow_guard_estimated_tokens: None,
context_window_tokens: None,
context_window_source: None,
budget_used_percent: None,
pressure_label: String::new(),
context_window_verified: false,
generated_at: String::new(),
note: "not assembled over the remote command surface".to_string(),
}
}
fn prompt_context(&self) -> DebugPromptContext {
DebugPromptContext {
schema_version: 0,
provider: String::new(),
model: self.snapshot.model.clone(),
system_prompt_state: String::new(),
tool_catalog_state: String::new(),
sections: Vec::new(),
tools: Vec::new(),
source_map: self.context_source_map(),
}
}
fn tool_snapshot(&self) -> Option<DebugToolSnapshot> {
None
}
fn inspect_cache(
&self,
) -> Result<
codewhale_command_contract::DebugCacheInspectionObservation,
DebugCacheInspectionUnavailable,
> {
Err(DebugCacheInspectionUnavailable::NoConcreteRoute)
}
fn remember_cache_inspection(
&mut self,
_inspection: codewhale_command_contract::facets::DebugPromptInspection,
) {
// Read-only surface: nothing to remember between remote executions.
}
/// The remote surface does not serve `/cache`; the honest encoding of
/// "not assembled" is absent telemetry, never zeros dressed as data.
fn cache_telemetry(&self) -> DebugCacheTelemetry {
DebugCacheTelemetry {
model: self.snapshot.model.clone(),
session_cache_rates: DebugCacheRates::default(),
history: Vec::new(),
calls: Vec::<DebugCacheCall>::new(),
calls_total: 0,
calls_currency: self.snapshot.tokens.cost.currency,
calls_legacy_coverage_unknown: false,
history_capacity: 0,
prefix_stability_pct: None,
prefix_checks_total: 0,
prefix_change_count: 0,
prefix_drift_count: 0,
prefix_context_updates: 0,
prefix_pin_reason: None,
prefix_last_miss_reason: None,
last_prefix_change_desc: None,
last_pinned_prefix_hash: None,
api_message_count: self.snapshot.tokens.api_message_count,
non_system_message_count: self.snapshot.tokens.chat_message_count,
}
}
}
struct SnapshotPresentation {
locale: Locale,
}
impl CommandPresentationContext for SnapshotPresentation {
fn translate(&self, key: &str, replacements: &[(&str, &str)]) -> Result<String, String> {
let message_id = resolve_message_id(key).ok_or("unknown translation key")?;
let template = tr(self.locale, message_id);
apply_named_replacements(&template, replacements)
.ok_or_else(|| "invalid translation replacement contract".to_string())
}
fn language_name(&self) -> &'static str {
self.locale.translation_target_name()
}
}
// ---------------------------------------------------------------------------
// Execution
// ---------------------------------------------------------------------------
/// Execute one built-in command against the snapshot, or `None` when the
/// command is outside the remote surface.
pub fn execute_builtin(
name: &str,
args: Option<&str>,
snapshot: &BuiltinCommandHostSnapshot,
) -> Option<BuiltinCommandExecution> {
let (info, handler) = resolve(name)?;
let CommandHandler::Contextual {
capabilities,
handler: run,
} = handler
else {
return None;
};
// The capability mask is the second gate: a handler that grew a new
// facet requirement is refused until the snapshot grows the data for it.
if !PROVIDED.contains(capabilities) {
return None;
}
let mut diagnostics = SnapshotDiagnostics { snapshot };
let mut presentation = SnapshotPresentation {
locale: snapshot.locale,
};
let contexts = CommandContexts::empty()
.with_debug_diagnostics(&mut diagnostics)
.with_presentation(&mut presentation);
let result: portable_debug::CommandResult = run(contexts, args);
Some(BuiltinCommandExecution {
name: info.name.to_string(),
text: result.message.unwrap_or_default(),
is_error: result.is_error,
text_only: result.action.is_none(),
})
}
/// Whether `name` is on the remote surface at all. Shares [`resolve`] with
/// [`execute_builtin`], so the two gates can never disagree about a name.
pub fn serves_builtin(name: &str) -> bool {
resolve(name).is_some()
}
/// The portable handler a raised spelling (canonical name or alias) names,
/// gated by the [`SERVED`] allowlist and the [`PROVIDED`] capability mask.
fn resolve(
name: &str,
) -> Option<(
&'static codewhale_command_contract::metadata::CommandInfo,
CommandHandler<portable_debug::CommandResult>,
)> {
let spelling = name.trim_start_matches('/').to_ascii_lowercase();
let (info, handler) = portable_debug::portable_handlers()
.into_iter()
.find(|(info, _)| {
info.name == spelling.as_str() || info.aliases.contains(&spelling.as_str())
})?;
// The allowlist gates on the canonical name, so an alias resolves to the
// same served command its canonical spelling is.
if !SERVED.contains(&info.name) {
return None;
}
// A non-contextual handler asks nothing of the host, so it has nothing to
// read and is refused here rather than served an empty envelope.
if !matches!(handler, CommandHandler::Contextual { capabilities, .. } if PROVIDED.contains(capabilities))
{
return None;
}
Some((info, handler))
}
#[cfg(test)]
mod tests {
use super::*;
use codewhale_command_contract::facets::DebugSystemPrompt;
use codewhale_command_contract::types::{CommandCurrency, CommandMode};
fn snapshot() -> BuiltinCommandHostSnapshot {
BuiltinCommandHostSnapshot {
mode: CommandMode::Agent,
mode_label: "WORK".to_string(),
locale: Locale::En,
model: "deepseek-v4-pro".to_string(),
system_prompt: DebugSystemPrompt::Text("You are helpful.".to_string()),
tokens: DebugTokenProjection {
active_context_used: 4_200,
context_window: 131_072,
last_input: Some(3_900),
last_output: Some(312),
cache_hit: Some(3_400),
cache_miss: Some(500),
total_tokens: 21_000,
cache_write_tokens: 640,
api_message_count: 12,
chat_message_count: 8,
model: "deepseek-v4-pro".to_string(),
cost: DebugCostProjection {
currency: CommandCurrency::Usd,
total: 0.012_5,
parent_turns: 0.012,
subagents: 0.000_5,
display_floor: 0.0,
priced_turns: 6,
unpriced_turns: 0,
legacy_coverage_unknown: false,
user_declared_estimates: false,
itemized_turns: 6,
route_amounts: Vec::new(),
turn_history_capacity: 40,
unpriced_reason_labels: Vec::new(),
unpriced_classes: Vec::new(),
pricing_provenances: Vec::new(),
live_pricing_defects: Vec::new(),
unusable_pricing_defects: Vec::new(),
route_receipts: Vec::new(),
},
},
}
}
#[test]
fn serves_system_with_mode_label_and_prompt() {
let execution = execute_builtin("system", None, &snapshot()).expect("served");
assert_eq!(execution.name, "system");
assert!(!execution.is_error);
assert!(execution.text_only);
assert!(execution.text.contains("WORK"));
assert!(execution.text.contains("You are helpful."));
}
#[test]
fn serves_tokens_with_published_usage() {
let execution = execute_builtin("/tokens", None, &snapshot()).expect("served");
assert_eq!(execution.name, "tokens");
assert!(execution.text.contains("21,000") || execution.text.contains("21000"));
assert!(execution.text.contains("deepseek-v4-pro"));
}
#[test]
fn serves_cost_with_the_priced_subtotal() {
let execution = execute_builtin("cost", None, &snapshot()).expect("served");
assert!(!execution.is_error);
assert!(!execution.text.is_empty());
}
#[test]
fn resolves_engine_aliases_to_the_canonical_command() {
let execution = execute_builtin("xitong", None, &snapshot()).expect("served");
assert_eq!(execution.name, "system");
}
#[test]
fn the_served_gate_agrees_with_execution_for_every_spelling() {
// The route checks `serves_builtin` before assembling any thread
// state, then calls `execute_builtin`: a name the first gate admits
// must never be refused by the second, and vice versa.
let snapshot = snapshot();
let mut spellings: Vec<String> = SERVED.iter().map(|name| (*name).to_string()).collect();
for (info, _) in portable_debug::portable_handlers() {
spellings.push(info.name.to_string());
spellings.extend(info.aliases.iter().map(|alias| (*alias).to_string()));
}
spellings.push("nope".to_string());
for name in spellings {
for raised in [name.clone(), format!("/{name}"), name.to_uppercase()] {
let served = serves_builtin(&raised);
let executed = execute_builtin(&raised, None, &snapshot).is_some();
assert_eq!(served, executed, "{raised}");
}
}
}
#[test]
fn refuses_commands_outside_the_phase1_allowlist() {
// /balance is a host action (FetchBalance), /cache needs assembled
// telemetry, /undo needs the undo facet: none are honestly served.
for name in [
"balance", "cache", "undo", "retry", "context", "tools", "nope",
] {
assert!(execute_builtin(name, None, &snapshot()).is_none(), "{name}");
}
}
}
+15 -9
View File
@@ -2456,17 +2456,23 @@ pub(crate) struct PresentationAdapter<'a> {
host: SharedCommandHost<'a>,
}
/// Resolve one stable key to the current catalog id, in the order every host
/// adapter must use. Kept in one place so the terminal host and the remote
/// snapshot-backed adapter cannot answer the same key differently.
pub(crate) fn resolve_message_id(key: &str) -> Option<MessageId> {
key_to_utility_message_id(key)
.or_else(|| key_to_project_message_id(key))
.or_else(|| key_to_plugin_message_id(key))
.or_else(|| key_to_session_message_id(key))
.or_else(|| diagnostics_messages::resolve(key))
.or_else(|| config_policy_messages::resolve(key))
.or_else(|| config_messages::resolve(key))
.or_else(|| core_navigation::resolve_message(key))
}
impl CommandPresentationContext for PresentationAdapter<'_> {
fn translate(&self, key: &str, replacements: &[(&str, &str)]) -> Result<String, String> {
let Some(message_id) = key_to_utility_message_id(key)
.or_else(|| key_to_project_message_id(key))
.or_else(|| key_to_plugin_message_id(key))
.or_else(|| key_to_session_message_id(key))
.or_else(|| diagnostics_messages::resolve(key))
.or_else(|| config_policy_messages::resolve(key))
.or_else(|| config_messages::resolve(key))
.or_else(|| core_navigation::resolve_message(key))
else {
let Some(message_id) = resolve_message_id(key) else {
return Err("unknown translation key".to_string());
};
let locale = self.host.app.borrow().ui_locale;
+14
View File
@@ -6,6 +6,7 @@
//! module keeps registry construction, user-command precedence, and the
//! fall-through behaviour.
mod builtin_execution;
mod config_group;
mod config_host;
mod config_main_host;
@@ -256,6 +257,19 @@ impl codewhale_engine::command_catalog::CommandCatalog for CommandTable {
.collect()
}
fn serves_builtin(&self, name: &str) -> bool {
builtin_execution::serves_builtin(name)
}
fn execute_builtin(
&self,
name: &str,
args: Option<&str>,
host: &codewhale_engine::command_catalog::BuiltinCommandHostSnapshot,
) -> Option<codewhale_engine::command_catalog::BuiltinCommandExecution> {
builtin_execution::execute_builtin(name, args, host)
}
fn user_commands(
&self,
workspace: &std::path::Path,
+1
View File
@@ -45,6 +45,7 @@ changelog or release notes, and this contributor record.
**Contributions**
- **[SamhandsomeLee](https://github.com/SamhandsomeLee)** — drove Responses request, URL and stream behavior from one checked dialect profile table ([#7261](https://github.com/codewhale-hq/Codewhale/pull/7261)).
- **[gaord](https://github.com/gaord)** — ran the read-only `/system`, `/tokens` and `/cost` builtins over the Runtime API through one gated terminal-host catalog ([#7277](https://github.com/codewhale-hq/Codewhale/pull/7277)).
</details>
+24
View File
@@ -2604,6 +2604,30 @@ thread deletion.
respect: a `binding: "host"` row is never submitted as a model prompt, and
a user command shadowing a builtin name wins that spelling.
- `POST /v1/threads/{id}/commands/builtin` — run one terminal built-in
against the thread and return its own answer: `{name, text, is_error,
text_only}`. The body is `{name, args?}`, where `name` takes the canonical
spelling or an alias, with or without the leading slash.
Only read-only diagnostics the server can answer from thread state are
served (`/system`, `/tokens`, `/cost`); the served set lives in the
terminal host and is gated a second time by the data each portable handler
declares it needs, so a route cannot widen without the host widening
first. Any other name — including one no host knows — answers 404. A
thread whose engine is not loaded, or that has no live context estimate
yet, answers 409 rather than printing zeros; the route never starts an
engine. `/system` reports the effective system prompt the engine sends, and
`/tokens` counts the live session transcript. Like every other POST route,
it answers 403 to a watch-only device token, so a watch client cannot read
`/tokens` or `/cost` through it.
`text_only` is `false` when the command also asked the terminal host for an
action (a balance fetch, a snapshot restore): the text is still what the
command answered, but no client can perform that action on the host's
behalf. These executions keep no command receipt — every served handler is
pure over the snapshot — so a client must not look for one. Anything that
mutates the session stays a terminal-side command.
**Hooks**
- `GET /v1/hooks[?thread_id=...]` — `{workspace, enabled, hooks: [...],
problems: [...]}`: the hook set Runtime API threads run for that workspace
+2 -1
View File
@@ -188,6 +188,7 @@ registration index.
| DELETE | `/v1/threads/{id}/approval-grants/{grant_id}` |
| POST | `/v1/threads/{id}/commands` |
| DELETE / GET | `/v1/threads/{id}/commands/{operation_key}` |
| POST | `/v1/threads/{id}/commands/builtin` |
| POST | `/v1/threads/{id}/compact` |
| GET | `/v1/threads/{id}/context` |
| GET | `/v1/threads/{id}/events` |
@@ -259,4 +260,4 @@ registration index.
| GET / POST | `/v1/worktrees` |
| POST | `/v1/worktrees/remove` |
The source contains 239 distinct literal paths in this table.
The source contains 240 distinct literal paths in this table.
+18
View File
@@ -1936,6 +1936,24 @@ POST 前保留预期指纹,并在响应及按键 GET 恢复时核对,之后
`binding: "host"` 的行永不会被作为模型提示词提交,且
占用内置名称的用户命令在该拼写上胜出。
- `POST /v1/threads/{id}/commands/builtin` — 针对该线程运行一个终端内置命令,
并返回它自己的回答:`{name, text, is_error, text_only}`。请求体为
`{name, args?}`,其中 `name` 可用规范拼写或别名,带不带前导斜杠均可。
只提供服务器能从线程状态回答的只读诊断命令(`/system`、`/tokens`、
`/cost`);可提供的命令集合由终端宿主持有,并由各可移植处理器声明所需的
数据再把关一次,因此路由不会在宿主未先放开时自行扩展。其他任何名称——包括
没有任何宿主认识的名称——都返回 404。线程的引擎未加载,或尚无实时上下文
估算时返回 409,而不是打印零值;该路由从不启动引擎。`/system` 报告引擎实际
发送的有效系统提示词,`/tokens` 统计实时会话的对话记录。与其他 POST 路由一样,
它对仅用于观看的设备令牌返回 403,因此观看客户端无法通过它读取 `/tokens`
或 `/cost`。
当命令还向终端宿主请求了某个操作(余额获取、快照恢复)时,`text_only` 为
`false`:文本仍是命令的回答,但没有任何客户端能代宿主执行该操作。这些执行不
保留命令回执——每个提供的处理器对快照都是纯函数——因此客户端不应寻找回执。
任何会修改会话的操作仍是终端侧命令。
**钩子**
- `GET /v1/hooks[?thread_id=...]` — `{workspace, enabled, hooks: [...],
problems: [...]}`:Runtime API 线程为该工作区运行的钩子集合
+8 -8
View File
@@ -334,8 +334,8 @@
"sources": [
{
"path": "docs/RUNTIME_API.md",
"sha256": "efd7e47eb07d52eb91c4472d42bfe0337f253be956014a5540200118b43b765e",
"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": "6ba5585f222d835e1552644e6e004cfd926500fc79c151c9eedd69c554331ad0",
"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
}
]
},
@@ -387,8 +387,8 @@
},
{
"path": "docs/CONTRIBUTORS.md",
"sha256": "5ab3b08305a8484d6a55ee6439edce471dc7960e68be9c91eaeb5bfd1239a9d8",
"content": "# Contributors\n\nCodewhale is built in the open with a growing community of contributors. Every\nissue report and pull request is real project work — welcome at any experience\nlevel. This is the full per-PR contributor record, grouped into release/time\nbands with the newest band first so it stays scannable. Expand any band to see\neveryone.\n\nFor the live list, see the\n[GitHub contributors page](https://github.com/codewhale-hq/CodeWhale/graphs/contributors),\n[`AUTHOR_MAP`](https://github.com/codewhale-hq/CodeWhale/blob/main/.github/AUTHOR_MAP),\nand [CHANGELOG.md](../CHANGELOG.md).\n\n<!-- contributor-wall:start -->\n## Everyone\n\n[![421 people built Codewhale](assets/contributors.png)](#everyone)\n\n421 people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts 203 of them. If your commits are missing there, add your commit email to your GitHub account and they will link.\n\n[0gl20shk0sbt36](https://github.com/0gl20shk0sbt36) · [1Git2Clone](https://github.com/1Git2Clone) · [20bytes](https://github.com/20bytes) · [47Cid](https://github.com/47Cid) · [7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) · [8sero](https://github.com/8sero) · [A-Corner](https://github.com/A-Corner) · [aboimpinto](https://github.com/aboimpinto) · [adity982](https://github.com/adity982) · [AdityaVG13](https://github.com/AdityaVG13) · [AGSaturn](https://github.com/AGSaturn) · [ai-dengwsh](https://github.com/ai-dengwsh) · [aiapienthusiast](https://github.com/aiapienthusiast) · [Aitensa](https://github.com/Aitensa) · [AiurArtanis](https://github.com/AiurArtanis) · [alitvak69](https://github.com/alitvak69) · [all-lopezg](https://github.com/all-lopezg) · [alvin1](https://github.com/alvin1) · [amuthantamil](https://github.com/amuthantamil) · [AnaheimEX](https://github.com/AnaheimEX) · [Andrea-Bruno](https://github.com/Andrea-Bruno) · [Angel-Hair](https://github.com/Angel-Hair) · [angus-guo](https://github.com/angus-guo) · [angziii](https://github.com/angziii) · [AnonymousUser443](https://github.com/AnonymousUser443) · [antarikshraya](https://github.com/antarikshraya) · [Anyexyz](https://github.com/Anyexyz) · [Apeiron0w0](https://github.com/Apeiron0w0) · [aqilaziz](https://github.com/aqilaziz) · [AresNing](https://github.com/AresNing) · [ArronAI007](https://github.com/ArronAI007) · [Artenx](https://github.com/Artenx) · [asdfg314284230](https://github.com/asdfg314284230) · [Asteria1028](https://github.com/Asteria1028) · [asto18089](https://github.com/asto18089) · [atmosuwiryo](https://github.com/atmosuwiryo) · [axobase001](https://github.com/axobase001) · [aznikline](https://github.com/aznikline) · [baendlorel](https://github.com/baendlorel) · [banqii](https://github.com/banqii) · [Beltran12138](https://github.com/Beltran12138) · [bevis-wong](https://github.com/bevis-wong) · [BH8GCJ](https://github.com/BH8GCJ) · [BigBenLabs](https://github.com/BigBenLabs) · [Biilow](https://github.com/Biilow) · [bistack](https://github.com/bistack) · [BrathonBai](https://github.com/BrathonBai) · [bruce6135](https://github.com/bruce6135) · [BryonGo](https://github.com/BryonGo) · [buiducnhat](https://github.com/buiducnhat) · [buko](https://github.com/buko) · [BX166](https://github.com/BX166) · [c020627](https://github.com/c020627) · [cacdcaecawae](https://github.com/cacdcaecawae) · [caeserchen](https://github.com/caeserchen) · [caiyilian](https://github.com/caiyilian) · [CCChisato](https://github.com/CCChisato) · [ccomma](https://github.com/ccomma) · [cenab](https://github.com/cenab) · [ChaceLyee2101](https://github.com/ChaceLyee2101) · [chatman-media](https://github.com/chatman-media) · [chennest](https://github.com/chennest) · [chinaqy110](https://github.com/chinaqy110) · [chnjames](https://github.com/chnjames) · [chuntseevolving](https://github.com/chuntseevolving) · [ci4ic4](https://github.com/ci4ic4) · [cian-lien](https://github.com/cian-lien) · [cmdcorp6534](https://github.com/cmdcorp6534) · [cmyyy](https://github.com/cmyyy) · [codepgq](https://github.com/codepgq) · [Colorful-glassblock](https://github.com/Colorful-glassblock) · [ComeFromTheMars](https://github.com/ComeFromTheMars) · [CrepuscularIRIS](https://github.com/CrepuscularIRIS) · [ct-jaryn](https://github.com/ct-jaryn) · [ctxyao](https://github.com/ctxyao) · [cy2311](https://github.com/cy2311) · [cyq1017](https://github.com/cyq1017) · [czf0718](https://github.com/czf0718) · [dajiaohuang](https://github.com/dajiaohuang) · [DarrellThomas](https://github.com/DarrellThomas) · [demian-welt](https://github.com/demian-welt) · [Dev-next-gen](https://github.com/Dev-next-gen) · [dfwqdyl-ui](https://github.com/dfwqdyl-ui) · [DI-HUO-MING-YI](https://github.com/DI-HUO-MING-YI) · [DieMoe233](https://github.com/DieMoe233) · [DingYong4223Line truncated
"sha256": "c49c47a737cff6f934f4c92af392b3c85e11baf9f7cdf870d284c8db9e6a86e1",
"content": "# Contributors\n\nCodewhale is built in the open with a growing community of contributors. Every\nissue report and pull request is real project work — welcome at any experience\nlevel. This is the full per-PR contributor record, grouped into release/time\nbands with the newest band first so it stays scannable. Expand any band to see\neveryone.\n\nFor the live list, see the\n[GitHub contributors page](https://github.com/codewhale-hq/CodeWhale/graphs/contributors),\n[`AUTHOR_MAP`](https://github.com/codewhale-hq/CodeWhale/blob/main/.github/AUTHOR_MAP),\nand [CHANGELOG.md](../CHANGELOG.md).\n\n<!-- contributor-wall:start -->\n## Everyone\n\n[![421 people built Codewhale](assets/contributors.png)](#everyone)\n\n421 people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts 203 of them. If your commits are missing there, add your commit email to your GitHub account and they will link.\n\n[0gl20shk0sbt36](https://github.com/0gl20shk0sbt36) · [1Git2Clone](https://github.com/1Git2Clone) · [20bytes](https://github.com/20bytes) · [47Cid](https://github.com/47Cid) · [7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) · [8sero](https://github.com/8sero) · [A-Corner](https://github.com/A-Corner) · [aboimpinto](https://github.com/aboimpinto) · [adity982](https://github.com/adity982) · [AdityaVG13](https://github.com/AdityaVG13) · [AGSaturn](https://github.com/AGSaturn) · [ai-dengwsh](https://github.com/ai-dengwsh) · [aiapienthusiast](https://github.com/aiapienthusiast) · [Aitensa](https://github.com/Aitensa) · [AiurArtanis](https://github.com/AiurArtanis) · [alitvak69](https://github.com/alitvak69) · [all-lopezg](https://github.com/all-lopezg) · [alvin1](https://github.com/alvin1) · [amuthantamil](https://github.com/amuthantamil) · [AnaheimEX](https://github.com/AnaheimEX) · [Andrea-Bruno](https://github.com/Andrea-Bruno) · [Angel-Hair](https://github.com/Angel-Hair) · [angus-guo](https://github.com/angus-guo) · [angziii](https://github.com/angziii) · [AnonymousUser443](https://github.com/AnonymousUser443) · [antarikshraya](https://github.com/antarikshraya) · [Anyexyz](https://github.com/Anyexyz) · [Apeiron0w0](https://github.com/Apeiron0w0) · [aqilaziz](https://github.com/aqilaziz) · [AresNing](https://github.com/AresNing) · [ArronAI007](https://github.com/ArronAI007) · [Artenx](https://github.com/Artenx) · [asdfg314284230](https://github.com/asdfg314284230) · [Asteria1028](https://github.com/Asteria1028) · [asto18089](https://github.com/asto18089) · [atmosuwiryo](https://github.com/atmosuwiryo) · [axobase001](https://github.com/axobase001) · [aznikline](https://github.com/aznikline) · [baendlorel](https://github.com/baendlorel) · [banqii](https://github.com/banqii) · [Beltran12138](https://github.com/Beltran12138) · [bevis-wong](https://github.com/bevis-wong) · [BH8GCJ](https://github.com/BH8GCJ) · [BigBenLabs](https://github.com/BigBenLabs) · [Biilow](https://github.com/Biilow) · [bistack](https://github.com/bistack) · [BrathonBai](https://github.com/BrathonBai) · [bruce6135](https://github.com/bruce6135) · [BryonGo](https://github.com/BryonGo) · [buiducnhat](https://github.com/buiducnhat) · [buko](https://github.com/buko) · [BX166](https://github.com/BX166) · [c020627](https://github.com/c020627) · [cacdcaecawae](https://github.com/cacdcaecawae) · [caeserchen](https://github.com/caeserchen) · [caiyilian](https://github.com/caiyilian) · [CCChisato](https://github.com/CCChisato) · [ccomma](https://github.com/ccomma) · [cenab](https://github.com/cenab) · [ChaceLyee2101](https://github.com/ChaceLyee2101) · [chatman-media](https://github.com/chatman-media) · [chennest](https://github.com/chennest) · [chinaqy110](https://github.com/chinaqy110) · [chnjames](https://github.com/chnjames) · [chuntseevolving](https://github.com/chuntseevolving) · [ci4ic4](https://github.com/ci4ic4) · [cian-lien](https://github.com/cian-lien) · [cmdcorp6534](https://github.com/cmdcorp6534) · [cmyyy](https://github.com/cmyyy) · [codepgq](https://github.com/codepgq) · [Colorful-glassblock](https://github.com/Colorful-glassblock) · [ComeFromTheMars](https://github.com/ComeFromTheMars) · [CrepuscularIRIS](https://github.com/CrepuscularIRIS) · [ct-jaryn](https://github.com/ct-jaryn) · [ctxyao](https://github.com/ctxyao) · [cy2311](https://github.com/cy2311) · [cyq1017](https://github.com/cyq1017) · [czf0718](https://github.com/czf0718) · [dajiaohuang](https://github.com/dajiaohuang) · [DarrellThomas](https://github.com/DarrellThomas) · [demian-welt](https://github.com/demian-welt) · [Dev-next-gen](https://github.com/Dev-next-gen) · [dfwqdyl-ui](https://github.com/dfwqdyl-ui) · [DI-HUO-MING-YI](https://github.com/DI-HUO-MING-YI) · [DieMoe233](https://github.com/DieMoe233) · [DingYong4223Line truncated
},
{
"path": "docs/RELEASE_CHECKLIST.md",
@@ -403,13 +403,13 @@
"sources": [
{
"path": "docs/RUNTIME_API.md",
"sha256": "efd7e47eb07d52eb91c4472d42bfe0337f253be956014a5540200118b43b765e",
"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": "6ba5585f222d835e1552644e6e004cfd926500fc79c151c9eedd69c554331ad0",
"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
},
{
"path": "docs/RUNTIME_ROUTES.md",
"sha256": "f32f17d117a9edf23dcd6a0bb82f938fa335f1afee47f9170d9ae25d733e4ee6",
"content": "# Runtime HTTP route inventory\n\nThis table is generated from literal route registrations in\n`crates/app-server/src/runtime_api.rs`. It names the paths and methods that\nthe current source registers, including native-client and compatibility\nsurfaces. A registered path is not a grant of permission or a promise that\na feature is enabled in your installed version.\n\nThe [Runtime API contract](RUNTIME_API.md) explains request fields, events,\nauthorization and compatibility. This inventory is a navigation index, not\na replacement for those schemas; routes used by the native client can have\nclient-specific contracts. Provider calls, file mutations and connected-service\nactions remain subject to their documented authority boundaries.\n\nRun `node web/scripts/check-runtime-routes.mjs --write` after changing a\nregistration; `npm run check:docs` refuses a stale inventory. Dynamic nested\nrouters, fallback handlers and static-asset mounts are outside this literal\nregistration index.\n\n| HTTP method | Path |\n| --- | --- |\n| GET | `/` |\n| GET | `/__codewhale/bootstrap/{nonce}` |\n| GET | `/__codewhale/mobile/bootstrap/{nonce}` |\n| POST | `/__codewhale/mobile/session` |\n| POST | `/__codewhale/mobile/stream-ticket` |\n| POST | `/__codewhale/web/stream-ticket` |\n| GET | `/assets/codewhale-192.png` |\n| GET | `/assets/codewhale-web.css` |\n| GET | `/assets/codewhale-web.js` |\n| GET | `/health` |\n| GET | `/mobile` |\n| GET | `/mobile/` |\n| DELETE / GET / PUT | `/v1/account/model-access` |\n| POST | `/v1/agent-mail` |\n| GET | `/v1/agent-runs` |\n| GET | `/v1/agent-runs/{run_id}` |\n| POST | `/v1/agent-runs/{run_id}/cancel` |\n| GET | `/v1/agents/external` |\n| GET | `/v1/agents/external/{id}/options` |\n| GET | `/v1/approvals` |\n| POST | `/v1/approvals/{approval_id}` |\n| GET | `/v1/apps/avatars` |\n| GET | `/v1/apps/avatars/{handle}/atlases/{page}` |\n| GET / POST | `/v1/apps/marketplaces` |\n| DELETE / GET | `/v1/apps/marketplaces/{name}` |\n| POST | `/v1/apps/marketplaces/{name}/install` |\n| POST | `/v1/apps/mcp/desktop-browser` |\n| GET / POST | `/v1/apps/mcp/imports` |\n| GET / POST | `/v1/apps/mcp/servers` |\n| DELETE / GET / PATCH | `/v1/apps/mcp/servers/{name}` |\n| POST | `/v1/apps/mcp/servers/{name}/disable` |\n| POST | `/v1/apps/mcp/servers/{name}/enable` |\n| POST | `/v1/apps/mcp/servers/{name}/reconnect` |\n| GET | `/v1/apps/mcp/tools` |\n| GET | `/v1/apps/plugins` |\n| DELETE / GET | `/v1/apps/plugins/{selector}` |\n| POST | `/v1/apps/plugins/{selector}/disable` |\n| POST | `/v1/apps/plugins/{selector}/enable` |\n| POST | `/v1/apps/plugins/{selector}/restage` |\n| POST | `/v1/apps/plugins/{selector}/revoke` |\n| POST | `/v1/apps/plugins/{selector}/trust` |\n| GET | `/v1/apps/plugins/{selector}/ui` |\n| GET | `/v1/apps/plugins/{selector}/ui/{*path}` |\n| POST | `/v1/apps/plugins/{selector}/update` |\n| GET | `/v1/apps/plugins/catalog` |\n| POST | `/v1/apps/plugins/catalog/{id}/disable` |\n| POST | `/v1/apps/plugins/catalog/{id}/enable` |\n| PUT | `/v1/apps/plugins/catalog/{id}/settings` |\n| POST | `/v1/apps/plugins/import/dsh/preview` |\n| POST | `/v1/apps/plugins/install` |\n| GET / POST | `/v1/automations` |\n| DELETE / GET / PATCH | `/v1/automations/{id}` |\n| GET | `/v1/automations/{id}/missed` |\n| PUT | `/v1/automations/{id}/missed/policy` |\n| POST | `/v1/automations/{id}/missed/run` |\n| POST | `/v1/automations/{id}/missed/skip` |\n| POST | `/v1/automations/{id}/pause` |\n| POST | `/v1/automations/{id}/resume` |\n| POST | `/v1/automations/{id}/run` |\n| GET | `/v1/automations/{id}/runs` |\n| GET | `/v1/automations/missed` |\n| GET | `/v1/changes` |\n| GET / POST | `/v1/cloud` |\n| POST | `/v1/cloud/attach` |\n| GET | `/v1/commands` |\n| GET / POST | `/v1/config` |\n| POST | `/v1/config/reload` |\n| GET | `/v1/constitution` |\n| POST | `/v1/constitution/preview` |\n| GET | `/v1/crashes` |\n| GET | `/v1/crashes/{name}` |\n| GET | `/v1/definition` |\n| GET | `/v1/diagnostics` |\n| GET | `/v1/diff` |\n| POST | `/v1/fleet/placement` |\n| GET | `/v1/fleet/profiles` |\n| GET | `/v1/fleet/profiles/{id}/document` |\n| GET / POST | `/v1/fleet/runs` |\n| GET | `/v1/fleet/runs/{run_id}` |\n| GET | `/v1/fleet/runs/{run_id}/events` |\n| GET | `/v1/fleet/runs/{run_id}/events/replay` |\n| GET | `/v1/fleet/runs/{run_id}/receipts` |\n| GET | `/v1/fleet/runs/{run_id}/receipts/{task_id}` |\n| GET | `/v1/fleet/runs/{run_id}/receipts/{task_id}/evidence` |\n| POST | `/v1/fleet/runs/{run_id}/start` |\n| POST | `/v1/fleet/runs/{run_id}/stop` |\n| GET | `/v1/fleet/runs/{run_id}/workers` |\n| GET | `/v1/fleet/workers/{worker_id}` |\n| POST | `/v1/fleet/workers/{worker_id}/interrupt` |\n| POST | `/v1/fleet/workers/{worker_id}/restart` |\n| POST | `/v1/fleet/workers/{worker_id}/stop` |\n| GET | `/v1/git` |\n| POST | `/v1/git/branch` |\n| POST | `/v1/git/commit` |\n| POST | `/v1/git/discard` |\n| GET | `/v1/git/graph` |\n| POST | `/v1/git/push` |\n| POST | `/v1/git/stage` |\n| POST | `/v1Line truncated
"sha256": "9288e60562a70291dfa98663bfecb410da7fbfa1b685092bf4e40dee843a1032",
"content": "# Runtime HTTP route inventory\n\nThis table is generated from literal route registrations in\n`crates/app-server/src/runtime_api.rs`. It names the paths and methods that\nthe current source registers, including native-client and compatibility\nsurfaces. A registered path is not a grant of permission or a promise that\na feature is enabled in your installed version.\n\nThe [Runtime API contract](RUNTIME_API.md) explains request fields, events,\nauthorization and compatibility. This inventory is a navigation index, not\na replacement for those schemas; routes used by the native client can have\nclient-specific contracts. Provider calls, file mutations and connected-service\nactions remain subject to their documented authority boundaries.\n\nRun `node web/scripts/check-runtime-routes.mjs --write` after changing a\nregistration; `npm run check:docs` refuses a stale inventory. Dynamic nested\nrouters, fallback handlers and static-asset mounts are outside this literal\nregistration index.\n\n| HTTP method | Path |\n| --- | --- |\n| GET | `/` |\n| GET | `/__codewhale/bootstrap/{nonce}` |\n| GET | `/__codewhale/mobile/bootstrap/{nonce}` |\n| POST | `/__codewhale/mobile/session` |\n| POST | `/__codewhale/mobile/stream-ticket` |\n| POST | `/__codewhale/web/stream-ticket` |\n| GET | `/assets/codewhale-192.png` |\n| GET | `/assets/codewhale-web.css` |\n| GET | `/assets/codewhale-web.js` |\n| GET | `/health` |\n| GET | `/mobile` |\n| GET | `/mobile/` |\n| DELETE / GET / PUT | `/v1/account/model-access` |\n| POST | `/v1/agent-mail` |\n| GET | `/v1/agent-runs` |\n| GET | `/v1/agent-runs/{run_id}` |\n| POST | `/v1/agent-runs/{run_id}/cancel` |\n| GET | `/v1/agents/external` |\n| GET | `/v1/agents/external/{id}/options` |\n| GET | `/v1/approvals` |\n| POST | `/v1/approvals/{approval_id}` |\n| GET | `/v1/apps/avatars` |\n| GET | `/v1/apps/avatars/{handle}/atlases/{page}` |\n| GET / POST | `/v1/apps/marketplaces` |\n| DELETE / GET | `/v1/apps/marketplaces/{name}` |\n| POST | `/v1/apps/marketplaces/{name}/install` |\n| POST | `/v1/apps/mcp/desktop-browser` |\n| GET / POST | `/v1/apps/mcp/imports` |\n| GET / POST | `/v1/apps/mcp/servers` |\n| DELETE / GET / PATCH | `/v1/apps/mcp/servers/{name}` |\n| POST | `/v1/apps/mcp/servers/{name}/disable` |\n| POST | `/v1/apps/mcp/servers/{name}/enable` |\n| POST | `/v1/apps/mcp/servers/{name}/reconnect` |\n| GET | `/v1/apps/mcp/tools` |\n| GET | `/v1/apps/plugins` |\n| DELETE / GET | `/v1/apps/plugins/{selector}` |\n| POST | `/v1/apps/plugins/{selector}/disable` |\n| POST | `/v1/apps/plugins/{selector}/enable` |\n| POST | `/v1/apps/plugins/{selector}/restage` |\n| POST | `/v1/apps/plugins/{selector}/revoke` |\n| POST | `/v1/apps/plugins/{selector}/trust` |\n| GET | `/v1/apps/plugins/{selector}/ui` |\n| GET | `/v1/apps/plugins/{selector}/ui/{*path}` |\n| POST | `/v1/apps/plugins/{selector}/update` |\n| GET | `/v1/apps/plugins/catalog` |\n| POST | `/v1/apps/plugins/catalog/{id}/disable` |\n| POST | `/v1/apps/plugins/catalog/{id}/enable` |\n| PUT | `/v1/apps/plugins/catalog/{id}/settings` |\n| POST | `/v1/apps/plugins/import/dsh/preview` |\n| POST | `/v1/apps/plugins/install` |\n| GET / POST | `/v1/automations` |\n| DELETE / GET / PATCH | `/v1/automations/{id}` |\n| GET | `/v1/automations/{id}/missed` |\n| PUT | `/v1/automations/{id}/missed/policy` |\n| POST | `/v1/automations/{id}/missed/run` |\n| POST | `/v1/automations/{id}/missed/skip` |\n| POST | `/v1/automations/{id}/pause` |\n| POST | `/v1/automations/{id}/resume` |\n| POST | `/v1/automations/{id}/run` |\n| GET | `/v1/automations/{id}/runs` |\n| GET | `/v1/automations/missed` |\n| GET | `/v1/changes` |\n| GET / POST | `/v1/cloud` |\n| POST | `/v1/cloud/attach` |\n| GET | `/v1/commands` |\n| GET / POST | `/v1/config` |\n| POST | `/v1/config/reload` |\n| GET | `/v1/constitution` |\n| POST | `/v1/constitution/preview` |\n| GET | `/v1/crashes` |\n| GET | `/v1/crashes/{name}` |\n| GET | `/v1/definition` |\n| GET | `/v1/diagnostics` |\n| GET | `/v1/diff` |\n| POST | `/v1/fleet/placement` |\n| GET | `/v1/fleet/profiles` |\n| GET | `/v1/fleet/profiles/{id}/document` |\n| GET / POST | `/v1/fleet/runs` |\n| GET | `/v1/fleet/runs/{run_id}` |\n| GET | `/v1/fleet/runs/{run_id}/events` |\n| GET | `/v1/fleet/runs/{run_id}/events/replay` |\n| GET | `/v1/fleet/runs/{run_id}/receipts` |\n| GET | `/v1/fleet/runs/{run_id}/receipts/{task_id}` |\n| GET | `/v1/fleet/runs/{run_id}/receipts/{task_id}/evidence` |\n| POST | `/v1/fleet/runs/{run_id}/start` |\n| POST | `/v1/fleet/runs/{run_id}/stop` |\n| GET | `/v1/fleet/runs/{run_id}/workers` |\n| GET | `/v1/fleet/workers/{worker_id}` |\n| POST | `/v1/fleet/workers/{worker_id}/interrupt` |\n| POST | `/v1/fleet/workers/{worker_id}/restart` |\n| POST | `/v1/fleet/workers/{worker_id}/stop` |\n| GET | `/v1/git` |\n| POST | `/v1/git/branch` |\n| POST | `/v1/git/commit` |\n| POST | `/v1/git/discard` |\n| GET | `/v1/git/graph` |\n| POST | `/v1/git/push` |\n| POST | `/v1/git/stage` |\n| POST | `/v1Line truncated
}
]
},
+1 -1
View File
@@ -42,7 +42,7 @@ export const RELEASE_CONTRIBUTORS: string[] = [
* one. scripts/check-contributor-credit.py requires them here now; the release
* cut moves them into RELEASE_CONTRIBUTORS with that release's changelog block.
*/
export const UNRELEASED_CONTRIBUTORS: string[] = ["@luochen211", "@Lstarsky0", "@SamhandsomeLee"];
export const UNRELEASED_CONTRIBUTORS: string[] = ["@luochen211", "@Lstarsky0", "@SamhandsomeLee", "@gaord"];
/**
* Contributors who helped with reports, reproductions, and verification.