feat: add workspace runtime and worker controls

This commit is contained in:
2026-07-03 02:27:42 +09:00
parent 2eb90470bb
commit f2fead7ebd
10 changed files with 1550 additions and 42 deletions
+21 -7
View File
@@ -2,7 +2,7 @@ use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::{fs, io};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use crate::hosts::RemoteRuntimeConfig;
use crate::identity::WorkspaceIdentity;
@@ -16,7 +16,7 @@ const DEFAULT_LISTEN: &str = "127.0.0.1:8787";
const DEFAULT_FRONTEND_URL: &str = "http://127.0.0.1:5173";
const DEFAULT_MAX_RECORDS: usize = 200;
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceBackendConfigFile {
#[serde(default)]
@@ -29,7 +29,7 @@ pub struct WorkspaceBackendConfigFile {
pub runtimes: WorkspaceBackendRuntimesConfig,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceBackendServerConfig {
#[serde(default)]
@@ -40,7 +40,7 @@ pub struct WorkspaceBackendServerConfig {
pub static_assets_dir: Option<PathBuf>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceBackendDataConfig {
#[serde(default)]
@@ -51,21 +51,21 @@ pub struct WorkspaceBackendDataConfig {
pub embedded_runtime_store_root: Option<PathBuf>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceBackendLimitsConfig {
#[serde(default)]
pub max_records: Option<usize>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceBackendRuntimesConfig {
#[serde(default)]
pub remote: Vec<RemoteRuntimeConfigFile>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct RemoteRuntimeConfigFile {
pub id: String,
@@ -189,6 +189,20 @@ impl WorkspaceBackendConfigFile {
}
}
pub fn write_for_workspace(&self, workspace_root: impl AsRef<Path>) -> Result<()> {
let path = Self::path_for_workspace(workspace_root);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let raw = toml::to_string_pretty(self).map_err(|error| {
Error::Config(format!(
"failed to serialize workspace backend config: {error}"
))
})?;
fs::write(path, raw)?;
Ok(())
}
pub fn parse_str(raw: &str, path: impl AsRef<Path>) -> Result<Self> {
toml::from_str(raw).map_err(|error| {
Error::Config(format!(
+3
View File
@@ -266,6 +266,7 @@ pub struct WorkerSpawnRequest {
pub enum WorkerSpawnIntent {
WorkspaceCompanion,
WorkspaceOrchestrator,
WorkspaceCoding,
TicketRole {
ticket_id: String,
role: TicketWorkerRole,
@@ -2260,6 +2261,7 @@ fn embedded_profile_selector(intent: &WorkerSpawnIntent) -> ProfileSelector {
ProfileSelector::Builtin("builtin:companion".to_string())
}
WorkerSpawnIntent::WorkspaceOrchestrator => ProfileSelector::RuntimeDefault,
WorkerSpawnIntent::WorkspaceCoding => ProfileSelector::Builtin("builtin:coder".to_string()),
}
}
@@ -2680,6 +2682,7 @@ fn worker_spawn_intent_label(intent: &WorkerSpawnIntent) -> &'static str {
match intent {
WorkerSpawnIntent::WorkspaceCompanion => "workspace_companion",
WorkerSpawnIntent::WorkspaceOrchestrator => "workspace_orchestrator",
WorkerSpawnIntent::WorkspaceCoding => "workspace_coding",
WorkerSpawnIntent::TicketRole { role, .. } => match role {
TicketWorkerRole::Intake => "ticket_intake",
TicketWorkerRole::Orchestrator => "ticket_orchestrator",
+794 -4
View File
@@ -6,8 +6,9 @@ use axum::extract::{Path as AxumPath, Query, State};
use axum::http::header::CONTENT_TYPE;
use axum::http::{StatusCode, Uri};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::routing::{delete, get, post};
use axum::{Json, Router};
use chrono::{SecondsFormat, Utc};
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;
@@ -17,11 +18,13 @@ use crate::companion::{
CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse,
CompanionStatusResponse, CompanionTranscriptProjection,
};
use crate::config::{RemoteRuntimeConfigFile, WorkspaceBackendConfigFile};
use crate::hosts::{
ConfigBundleCheckResult, ConfigBundleSyncResult, DiagnosticSeverity, EmbeddedWorkerRuntime,
HostSummary, RemoteRuntimeConfig, RemoteWorkerRuntime, RuntimeDiagnostic, RuntimeRegistry,
RuntimeSummary, WorkerInputRequest, WorkerInputResult, WorkerLifecycleRequest,
WorkerLifecycleResult, WorkerSpawnRequest, WorkerSpawnResult, WorkerSummary,
WorkerLifecycleResult, WorkerOperationState, WorkerSpawnAcceptanceRequirement,
WorkerSpawnIntent, WorkerSpawnRequest, WorkerSpawnResult, WorkerSummary,
WorkerTranscriptProjection,
};
use crate::identity::WorkspaceIdentity;
@@ -35,8 +38,14 @@ use crate::records::{
use crate::repositories::{LocalRepositoryReader, RepositoryLogRead, RepositorySummary};
use crate::store::{ControlPlaneStore, WorkspaceRecord};
use crate::{Error, Result};
use worker_runtime::catalog::ConfigBundleRef;
use worker_runtime::catalog::{ConfigBundleRef, ProfileSelector};
use worker_runtime::config_bundle::ConfigBundle;
use worker_runtime::http_server::RuntimeHttpSummaryResponse;
use worker_runtime::interaction::{
WorkerInput as EmbeddedWorkerInput, WorkerInputKind as EmbeddedWorkerInputKind,
};
const EMBEDDED_WORKER_RUNTIME_ID: &str = "embedded-worker-runtime";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AuthConfig {
@@ -222,7 +231,30 @@ pub fn build_router(api: WorkspaceApi) -> Router {
)
.route("/api/hosts", get(list_hosts))
.route("/api/runtimes", get(list_runtimes))
.route("/api/workers", get(list_workers))
.route(
"/api/workers",
get(list_workers).post(create_workspace_worker),
)
.route(
"/api/workers/launch-options",
get(get_worker_launch_options),
)
.route(
"/api/settings/runtime-connections",
get(get_runtime_connection_settings),
)
.route(
"/api/settings/runtime-connections/remotes",
post(add_remote_runtime_connection),
)
.route(
"/api/settings/runtime-connections/remotes/{runtime_id}",
delete(delete_remote_runtime_connection),
)
.route(
"/api/settings/runtime-connections/remotes/{runtime_id}/test",
post(test_remote_runtime_connection),
)
.route("/api/companion/status", get(get_companion_status))
.route("/api/companion/transcript", get(get_companion_transcript))
.route("/api/companion/messages", post(post_companion_message))
@@ -321,6 +353,108 @@ pub struct RuntimeListResponse<T> {
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RuntimeConnectionSettingsResponse {
pub workspace_id: String,
pub embedded: RuntimeConnectionSummary,
pub remotes: Vec<RemoteRuntimeConnectionSummary>,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RuntimeConnectionSummary {
pub runtime_id: String,
pub display_name: String,
pub kind: String,
pub built_in: bool,
pub config_managed: bool,
pub active: bool,
pub can_spawn_worker: bool,
pub restart_required: bool,
pub status: String,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RemoteRuntimeConnectionSummary {
#[serde(flatten)]
pub summary: RuntimeConnectionSummary,
pub endpoint_configured: bool,
pub token_ref_configured: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RuntimeConnectionMutationResponse {
pub workspace_id: String,
pub restart_required: bool,
pub remotes: Vec<RemoteRuntimeConnectionSummary>,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Deserialize)]
pub struct AddRemoteRuntimeConnectionRequest {
pub runtime_id: String,
pub display_name: Option<String>,
pub endpoint: String,
pub token_ref: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RemoteRuntimeTestResponse {
pub workspace_id: String,
pub runtime_id: String,
pub checked_at: String,
pub state: String,
pub protocol_version: Option<String>,
pub compatibility_basis: String,
pub capabilities: Vec<String>,
pub health_result: String,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WorkerLaunchOptionsResponse {
pub workspace_id: String,
pub runtimes: Vec<WorkerLaunchRuntimeOption>,
pub profiles: Vec<WorkerLaunchProfileCandidate>,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WorkerLaunchRuntimeOption {
pub runtime_id: String,
pub display_name: String,
pub built_in: bool,
pub can_spawn_worker: bool,
pub status: String,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerLaunchProfileCandidate {
pub id: String,
pub label: String,
pub description: String,
}
#[derive(Debug, Deserialize)]
pub struct BrowserCreateWorkerRequest {
pub runtime_id: String,
pub display_name: String,
pub profile: String,
pub initial_text: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BrowserCreateWorkerResponse {
pub workspace_id: String,
pub runtime_id: String,
pub worker_id: String,
pub console_href: String,
pub worker: WorkerSummary,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RepositoryListResponse {
pub workspace_id: String,
@@ -599,6 +733,194 @@ async fn list_workers(
workers_response(api).map(Json)
}
async fn get_runtime_connection_settings(
State(api): State<WorkspaceApi>,
) -> ApiResult<Json<RuntimeConnectionSettingsResponse>> {
let local_config = load_workspace_backend_config_for_settings(&api)?;
Ok(Json(runtime_connection_settings_response(
&api,
&local_config,
)))
}
async fn add_remote_runtime_connection(
State(api): State<WorkspaceApi>,
Json(request): Json<AddRemoteRuntimeConnectionRequest>,
) -> ApiResult<Json<RuntimeConnectionMutationResponse>> {
validate_runtime_connection_request(&request)?;
let mut local_config = load_workspace_backend_config_for_settings(&api)?;
let id = request.runtime_id.trim().to_string();
if id == EMBEDDED_WORKER_RUNTIME_ID {
return Err(settings_bad_request(
"embedded_runtime_not_config_managed",
"the embedded Runtime is built in and cannot be managed from local remote Runtime config",
));
}
if request
.token_ref
.as_ref()
.is_some_and(|value| !value.trim().is_empty())
{
return Err(settings_bad_request(
"remote_runtime_token_ref_unsupported",
"remote Runtime token_ref persistence is not supported by this v0 browser settings surface",
));
}
if local_config
.runtimes
.remote
.iter()
.any(|remote| remote.id == id)
{
return Err(settings_bad_request(
"remote_runtime_already_exists",
"a remote Runtime connection with that id is already configured",
));
}
local_config.runtimes.remote.push(RemoteRuntimeConfigFile {
id,
endpoint: request.endpoint.trim().to_string(),
display_name: request
.display_name
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
token_ref: None,
});
write_workspace_backend_config_for_settings(&api, &local_config)?;
let mut response = runtime_connection_mutation_response(&api, &local_config);
response.diagnostics.push(settings_diagnostic(
"workspace_backend_config_rewritten",
DiagnosticSeverity::Info,
"Local Runtime connection config was rewritten from the typed schema; comments and formatting are not preserved in v0.",
));
Ok(Json(response))
}
async fn delete_remote_runtime_connection(
State(api): State<WorkspaceApi>,
AxumPath(runtime_id): AxumPath<String>,
) -> ApiResult<Json<RuntimeConnectionMutationResponse>> {
if runtime_id == EMBEDDED_WORKER_RUNTIME_ID {
return Err(settings_bad_request(
"embedded_runtime_not_config_managed",
"the embedded Runtime is built in and cannot be deleted from remote Runtime config",
));
}
let mut local_config = load_workspace_backend_config_for_settings(&api)?;
let before = local_config.runtimes.remote.len();
local_config
.runtimes
.remote
.retain(|remote| remote.id != runtime_id);
if before == local_config.runtimes.remote.len() {
return Err(Error::UnknownRuntime(runtime_id).into());
}
write_workspace_backend_config_for_settings(&api, &local_config)?;
let mut response = runtime_connection_mutation_response(&api, &local_config);
response.diagnostics.push(settings_diagnostic(
"workspace_backend_config_rewritten",
DiagnosticSeverity::Info,
"Local Runtime connection config was rewritten from the typed schema; comments and formatting are not preserved in v0.",
));
Ok(Json(response))
}
async fn test_remote_runtime_connection(
State(api): State<WorkspaceApi>,
AxumPath(runtime_id): AxumPath<String>,
) -> ApiResult<Json<RemoteRuntimeTestResponse>> {
let local_config = load_workspace_backend_config_for_settings(&api)?;
let remote = local_config
.runtimes
.remote
.iter()
.find(|remote| remote.id == runtime_id)
.ok_or_else(|| Error::UnknownRuntime(runtime_id.clone()))?;
Ok(Json(test_remote_runtime_config(&api, remote).await))
}
async fn get_worker_launch_options(
State(api): State<WorkspaceApi>,
) -> ApiResult<Json<WorkerLaunchOptionsResponse>> {
Ok(Json(worker_launch_options_response(&api)))
}
async fn create_workspace_worker(
State(api): State<WorkspaceApi>,
Json(request): Json<BrowserCreateWorkerRequest>,
) -> ApiResult<Json<BrowserCreateWorkerResponse>> {
let profile_selector = profile_selector_for_candidate(&request.profile).ok_or_else(|| {
settings_bad_request(
"unsupported_worker_profile",
"profile must be selected from Backend-published worker profile candidates",
)
})?;
let display_name = sanitize_worker_display_name(&request.display_name).ok_or_else(|| {
settings_bad_request(
"invalid_worker_display_name",
"display_name must contain at least one non-control character",
)
})?;
let initial_text = request.initial_text.trim().to_string();
let initial_input = if initial_text.is_empty() {
None
} else {
Some(EmbeddedWorkerInput {
kind: EmbeddedWorkerInputKind::User,
content: initial_text,
})
};
let result = api
.runtime
.spawn_worker(
&request.runtime_id,
WorkerSpawnRequest {
requested_worker_name: Some(display_name),
intent: WorkerSpawnIntent::WorkspaceCoding,
acceptance: if initial_input.is_some() {
WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 1,
}
} else {
WorkerSpawnAcceptanceRequirement::SocketReady
},
profile: Some(profile_selector),
initial_input,
},
)
.map_err(|err| err.into_error())?;
if result.state != WorkerOperationState::Accepted {
return Err(Error::RuntimeOperationFailed {
runtime_id: request.runtime_id.clone(),
code: "workspace_worker_create_failed".to_string(),
message: "Runtime did not complete worker creation".to_string(),
}
.into());
}
let worker = result.worker.ok_or_else(|| Error::RuntimeOperationFailed {
runtime_id: request.runtime_id.clone(),
code: "workspace_worker_create_missing_summary".to_string(),
message: "Runtime completed worker creation without returning a Worker summary".to_string(),
})?;
let runtime_id = worker.runtime_id.clone();
let worker_id = worker.worker_id.clone();
let console_href = format!(
"/runtimes/{}/workers/{}/console",
encode_path_segment(&runtime_id),
encode_path_segment(&worker_id)
);
Ok(Json(BrowserCreateWorkerResponse {
workspace_id: api.config.workspace_id,
runtime_id,
worker_id,
console_href,
worker,
diagnostics: result.diagnostics,
}))
}
async fn get_companion_status(
State(api): State<WorkspaceApi>,
) -> ApiResult<Json<CompanionStatusResponse>> {
@@ -914,6 +1236,424 @@ fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSu
})
}
fn load_workspace_backend_config_for_settings(
api: &WorkspaceApi,
) -> ApiResult<WorkspaceBackendConfigFile> {
WorkspaceBackendConfigFile::load_for_workspace(&api.config.workspace_root).map_err(|error| {
Error::Config(format!(
"failed to read workspace backend local config for Runtime connections: {}",
sanitize_backend_error(&error.to_string())
))
.into()
})
}
fn write_workspace_backend_config_for_settings(
api: &WorkspaceApi,
local_config: &WorkspaceBackendConfigFile,
) -> ApiResult<()> {
local_config
.write_for_workspace(&api.config.workspace_root)
.map_err(|error| {
Error::Config(format!(
"failed to write workspace backend local config for Runtime connections: {}",
sanitize_backend_error(&error.to_string())
))
.into()
})
}
fn runtime_connection_settings_response(
api: &WorkspaceApi,
local_config: &WorkspaceBackendConfigFile,
) -> RuntimeConnectionSettingsResponse {
RuntimeConnectionSettingsResponse {
workspace_id: api.config.workspace_id.clone(),
embedded: embedded_runtime_connection_summary(api),
remotes: remote_runtime_connection_summaries(api, local_config, false),
diagnostics: Vec::new(),
}
}
fn runtime_connection_mutation_response(
api: &WorkspaceApi,
local_config: &WorkspaceBackendConfigFile,
) -> RuntimeConnectionMutationResponse {
RuntimeConnectionMutationResponse {
workspace_id: api.config.workspace_id.clone(),
restart_required: true,
remotes: remote_runtime_connection_summaries(api, local_config, true),
diagnostics: vec![settings_diagnostic(
"runtime_registry_restart_required",
DiagnosticSeverity::Warning,
"Runtime connection config changed; restart the Workspace backend for the live Runtime registry to use the new config.",
)],
}
}
fn embedded_runtime_connection_summary(api: &WorkspaceApi) -> RuntimeConnectionSummary {
let active = api
.runtime
.list_runtimes(api.config.max_records.min(200))
.items
.into_iter()
.find(|runtime| runtime.runtime_id == EMBEDDED_WORKER_RUNTIME_ID);
match active {
Some(runtime) => RuntimeConnectionSummary {
runtime_id: runtime.runtime_id,
display_name: runtime.label,
kind: runtime.kind,
built_in: true,
config_managed: false,
active: runtime.status == "active",
can_spawn_worker: runtime.capabilities.can_spawn_worker,
restart_required: false,
status: runtime.status,
diagnostics: runtime.diagnostics,
},
None => RuntimeConnectionSummary {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
display_name: "Embedded Runtime".to_string(),
kind: "embedded_worker_runtime".to_string(),
built_in: true,
config_managed: false,
active: false,
can_spawn_worker: false,
restart_required: false,
status: "unavailable".to_string(),
diagnostics: vec![settings_diagnostic(
"embedded_runtime_unavailable",
DiagnosticSeverity::Warning,
"The built-in embedded Runtime is not active in the current Runtime registry projection.",
)],
},
}
}
fn remote_runtime_connection_summaries(
api: &WorkspaceApi,
local_config: &WorkspaceBackendConfigFile,
restart_required: bool,
) -> Vec<RemoteRuntimeConnectionSummary> {
let live_runtimes = api
.runtime
.list_runtimes(api.config.max_records.min(200))
.items;
local_config
.runtimes
.remote
.iter()
.map(|remote| {
let live = live_runtimes
.iter()
.find(|runtime| runtime.runtime_id == remote.id);
let (display_name, kind, active, can_spawn_worker, status, diagnostics) = match live {
Some(runtime) => (
runtime.label.clone(),
runtime.kind.clone(),
runtime.status == "active",
runtime.capabilities.can_spawn_worker,
runtime.status.clone(),
runtime.diagnostics.clone(),
),
None => (
remote
.display_name
.clone()
.unwrap_or_else(|| remote.id.clone()),
"remote_http".to_string(),
false,
false,
"configured_restart_required".to_string(),
if restart_required {
vec![settings_diagnostic(
"runtime_registry_restart_required",
DiagnosticSeverity::Warning,
"This remote Runtime config is persisted but not active until the Workspace backend restarts.",
)]
} else {
Vec::new()
},
),
};
RemoteRuntimeConnectionSummary {
summary: RuntimeConnectionSummary {
runtime_id: remote.id.clone(),
display_name,
kind,
built_in: false,
config_managed: true,
active,
can_spawn_worker,
restart_required,
status,
diagnostics,
},
endpoint_configured: !remote.endpoint.trim().is_empty(),
token_ref_configured: remote
.token_ref
.as_deref()
.is_some_and(|value| !value.trim().is_empty()),
}
})
.collect()
}
fn validate_runtime_connection_request(
request: &AddRemoteRuntimeConnectionRequest,
) -> ApiResult<()> {
validate_public_runtime_id(request.runtime_id.trim())?;
let endpoint = request.endpoint.trim();
if endpoint.is_empty() || !(endpoint.starts_with("http://") || endpoint.starts_with("https://"))
{
return Err(settings_bad_request(
"invalid_remote_runtime_endpoint",
"endpoint must be an absolute http or https URL",
));
}
if request
.display_name
.as_deref()
.is_some_and(|value| value.chars().any(char::is_control))
{
return Err(settings_bad_request(
"invalid_remote_runtime_display_name",
"display_name cannot contain control characters",
));
}
Ok(())
}
fn validate_public_runtime_id(runtime_id: &str) -> ApiResult<()> {
if runtime_id.is_empty() {
return Err(settings_bad_request(
"invalid_runtime_id",
"runtime_id must not be empty",
));
}
if runtime_id.len() > 96
|| !runtime_id
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
{
return Err(settings_bad_request(
"invalid_runtime_id",
"runtime_id may contain only ASCII letters, digits, '-', '_' and '.' and must be at most 96 characters",
));
}
Ok(())
}
async fn test_remote_runtime_config(
api: &WorkspaceApi,
remote: &RemoteRuntimeConfigFile,
) -> RemoteRuntimeTestResponse {
let checked_at = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
if remote
.token_ref
.as_deref()
.is_some_and(|value| !value.trim().is_empty())
{
return RemoteRuntimeTestResponse {
workspace_id: api.config.workspace_id.clone(),
runtime_id: remote.id.clone(),
checked_at,
state: "rejected".to_string(),
protocol_version: None,
compatibility_basis: "not_checked_token_ref_unsupported".to_string(),
capabilities: Vec::new(),
health_result: "not_checked".to_string(),
diagnostics: vec![settings_diagnostic(
"remote_runtime_token_ref_unsupported",
DiagnosticSeverity::Error,
"Remote Runtime test cannot use token_ref in v0; no token or secret value was exposed to the Browser.",
)],
};
}
let url = format!("{}/v1/runtime", remote.endpoint.trim_end_matches('/'));
let result = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()
.and_then(|client| client.get(url).build().map(|request| (client, request)));
let Ok((client, request)) = result else {
return remote_runtime_test_failed(
api,
remote,
checked_at,
"remote_runtime_invalid_request",
"failed to build a remote Runtime test request",
);
};
match client.execute(request).await {
Ok(response) if response.status().is_success() => {
match response.json::<RuntimeHttpSummaryResponse>().await {
Ok(summary) => RemoteRuntimeTestResponse {
workspace_id: api.config.workspace_id.clone(),
runtime_id: remote.id.clone(),
checked_at,
state: "compatible".to_string(),
protocol_version: None,
compatibility_basis: "worker-runtime /v1/runtime summary endpoint responded successfully; no protocol_version field was advertised".to_string(),
capabilities: Vec::new(),
health_result: format!("status={:?}", summary.runtime.status),
diagnostics: Vec::new(),
},
Err(_) => remote_runtime_test_failed(
api,
remote,
checked_at,
"remote_runtime_malformed_summary",
"remote Runtime test endpoint responded, but the summary payload was not recognized",
),
}
}
Ok(response) => remote_runtime_test_failed(
api,
remote,
checked_at,
"remote_runtime_unhealthy",
format!(
"remote Runtime test endpoint returned HTTP status {}",
response.status().as_u16()
),
),
Err(error) => remote_runtime_test_failed(
api,
remote,
checked_at,
"remote_runtime_test_failed",
sanitize_backend_error(&error.to_string()),
),
}
}
fn remote_runtime_test_failed(
api: &WorkspaceApi,
remote: &RemoteRuntimeConfigFile,
checked_at: String,
code: impl Into<String>,
message: impl Into<String>,
) -> RemoteRuntimeTestResponse {
RemoteRuntimeTestResponse {
workspace_id: api.config.workspace_id.clone(),
runtime_id: remote.id.clone(),
checked_at,
state: "failed".to_string(),
protocol_version: None,
compatibility_basis: "worker-runtime /v1/runtime summary endpoint test".to_string(),
capabilities: Vec::new(),
health_result: "failed".to_string(),
diagnostics: vec![settings_diagnostic(
code,
DiagnosticSeverity::Error,
message,
)],
}
}
fn worker_launch_options_response(api: &WorkspaceApi) -> WorkerLaunchOptionsResponse {
let runtimes = api
.runtime
.list_runtimes(api.config.max_records.min(200))
.items
.into_iter()
.map(|runtime| {
let built_in = runtime.runtime_id == EMBEDDED_WORKER_RUNTIME_ID;
WorkerLaunchRuntimeOption {
runtime_id: runtime.runtime_id,
display_name: runtime.label,
built_in,
can_spawn_worker: runtime.capabilities.can_spawn_worker,
status: runtime.status,
diagnostics: runtime.diagnostics,
}
})
.collect();
WorkerLaunchOptionsResponse {
workspace_id: api.config.workspace_id.clone(),
runtimes,
profiles: worker_profile_candidates(),
diagnostics: Vec::new(),
}
}
fn worker_profile_candidates() -> Vec<WorkerLaunchProfileCandidate> {
vec![
WorkerLaunchProfileCandidate {
id: "builtin:coder".to_string(),
label: "Coding Worker".to_string(),
description: "Built-in coding role profile for implementation work.".to_string(),
},
WorkerLaunchProfileCandidate {
id: "runtime_default".to_string(),
label: "Runtime default".to_string(),
description: "Use the selected Runtime's default profile.".to_string(),
},
]
}
fn profile_selector_for_candidate(profile: &str) -> Option<ProfileSelector> {
match profile {
"builtin:coder" => Some(ProfileSelector::Builtin("builtin:coder".to_string())),
"runtime_default" => Some(ProfileSelector::RuntimeDefault),
_ => None,
}
}
fn sanitize_worker_display_name(value: &str) -> Option<String> {
let display_name = value.trim();
if display_name.is_empty() || display_name.chars().any(char::is_control) {
None
} else {
Some(display_name.chars().take(80).collect())
}
}
fn encode_path_segment(value: &str) -> String {
value
.bytes()
.flat_map(|byte| match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
vec![byte as char]
}
_ => format!("%{byte:02X}").chars().collect(),
})
.collect()
}
fn settings_bad_request(code: &'static str, message: &'static str) -> ApiError {
Error::RuntimeOperationFailed {
runtime_id: "workspace-backend".to_string(),
code: code.to_string(),
message: message.to_string(),
}
.into()
}
fn settings_diagnostic(
code: impl Into<String>,
severity: DiagnosticSeverity,
message: impl Into<String>,
) -> RuntimeDiagnostic {
RuntimeDiagnostic {
code: code.into(),
severity,
message: message.into(),
}
}
fn sanitize_backend_error(message: &str) -> String {
let workspace_paths = ["/home/", "/Users/", "\\\\", ":\\"];
if workspace_paths
.iter()
.any(|needle| message.contains(needle))
{
"operation failed; backend-private path details were omitted".to_string()
} else {
message.to_string()
}
}
fn ensure_local_repository(api: &WorkspaceApi, repository_id: &str) -> Result<String> {
let canonical_repository_id = api.local_repository_id();
if LocalRepositoryReader::is_local_repository_id(repository_id, api.workspace_id()) {
@@ -1086,6 +1826,16 @@ impl IntoResponse for ApiError {
Error::RuntimeOperationFailed { code, .. } if code == "remote_runtime_unsupported" => {
StatusCode::NOT_IMPLEMENTED
}
Error::RuntimeOperationFailed { code, .. }
if code.starts_with("workspace_settings_")
|| code.starts_with("invalid_")
|| code.starts_with("unsupported_worker_profile")
|| code.ends_with("_already_exists")
|| code.ends_with("_not_config_managed")
|| code.ends_with("_unsupported") =>
{
StatusCode::BAD_REQUEST
}
Error::RuntimeOperationFailed { .. } => StatusCode::BAD_GATEWAY,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
@@ -1125,6 +1875,46 @@ mod tests {
const TEST_REPOSITORY_ID: &str = "local-0192f0e8-4d84-7d6e-a000-000000000001";
const TEST_CREATED_AT: &str = "2026-06-23T06:43:28Z";
#[test]
fn worker_profile_candidates_are_backend_published_and_mapped() {
let candidates = worker_profile_candidates();
assert!(
candidates
.iter()
.any(|candidate| candidate.id == "builtin:coder")
);
assert!(matches!(
profile_selector_for_candidate("builtin:coder"),
Some(ProfileSelector::Builtin(value)) if value == "builtin:coder"
));
assert!(profile_selector_for_candidate("free-text-profile").is_none());
}
#[test]
fn runtime_connection_request_validation_bounds_browser_input() {
let ok = AddRemoteRuntimeConnectionRequest {
runtime_id: "team-runtime_1".to_string(),
display_name: Some("Team Runtime".to_string()),
endpoint: "https://runtime.example".to_string(),
token_ref: None,
};
assert!(validate_runtime_connection_request(&ok).is_ok());
let bad_endpoint = AddRemoteRuntimeConnectionRequest {
endpoint: "/tmp/socket".to_string(),
..ok
};
assert!(validate_runtime_connection_request(&bad_endpoint).is_err());
}
#[test]
fn sanitized_errors_omit_backend_private_paths() {
let sanitized = sanitize_backend_error(
"failed to open /home/example/.yoi/workspace-backend.local.toml",
);
assert!(!sanitized.contains("/home/example"));
}
#[derive(Default)]
struct DeterministicExecutionBackend {
contexts: std::sync::Mutex<