Files
yoi/crates/workspace-server/src/server.rs
T

13268 lines
476 KiB
Rust

use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::{Component, Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
use axum::extract::{Path as AxumPath, Query, State};
use axum::http::header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH, LOCATION, ORIGIN, SET_COOKIE};
use axum::http::{HeaderMap, StatusCode, Uri};
use axum::response::{IntoResponse, Response};
use axum::routing::{delete, get, patch, post, put};
use axum::{Json, Router};
use chrono::{Duration, SecondsFormat, Utc};
use futures::{SinkExt, StreamExt};
use memory::backend::{
MemoryBackendHttpResponse, MemoryBackendOperation, MemoryConsolidateStagingOperation,
MemoryConsolidationOutput,
};
use protocol::stream::{decode_method, encode_event};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use ticket::{
MarkdownText, NewTicketEvent, TicketBackend, TicketBodyReplacement, TicketEventKind,
TicketIdOrSlug, TicketItemEdit, TicketReview, TicketReviewResult, TicketStateChange,
TicketTargetEdit, TicketWorkflowState,
};
use ticket::{
SqliteTicketBackend, TicketBackendOperation, TicketBackendOperationResult,
execute_ticket_backend_operation,
};
use tokio::net::TcpListener;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message as TungsteniteMessage;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use url::Url;
use uuid::Uuid;
use webauthn_rs::prelude::{
CreationChallengeResponse, Passkey, PasskeyAuthentication, PasskeyRegistration,
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse, Webauthn,
WebauthnBuilder,
};
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
use crate::auth::{
AuthPublicConfig, AuthenticatedUser, RequestActor, auth_error, is_expired, mint_secret, new_id,
new_user_code, normalize_handle, parse_cookie, resolve_request_actor, rfc3339_after,
session_set_cookie, token_hash,
};
use crate::authority::{
MemoryAuthority, ObjectiveAuthority, ObjectiveCreateInput, ObjectiveEditInput,
SqliteWorkspaceAuthority, TicketAuthority,
};
use crate::companion::{
CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse,
CompanionStatusResponse, CompanionTranscriptProjection,
};
use crate::config::{BackendRuntimesConfigFile, RemoteRuntimeConfigFile, resolve_remote_runtime};
use crate::hosts::{
ConfigBundleCheckResult, ConfigBundleSyncResult, DiagnosticSeverity, EmbeddedWorkerRuntime,
HostSummary, RemoteRuntimeConfig, RemoteWorkerRuntime, RuntimeDiagnostic, RuntimeRegistry,
RuntimeRegistryError, RuntimeRegistryUnregisterResult, RuntimeSummary, WorkerCapabilitySummary,
WorkerCompletionsRequest, WorkerCompletionsResult, WorkerImplementationSummary,
WorkerInputKind, WorkerInputRequest, WorkerInputResult, WorkerLifecycleRequest,
WorkerLifecycleResult, WorkerOperationState, WorkerRestoreResult,
WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent, WorkerSpawnRequest, WorkerSpawnResult,
WorkerSpawnWorkingDirectoryRequest, WorkerSummary, WorkerWorkspaceSummary,
};
use crate::identity::WorkspaceIdentity;
use crate::memory_backend::execute_memory_backend_operation_with_authority;
use crate::memory_staging::{
MemoryStagingListResponse, list_memory_staging_from_authority,
memory_staging_backlog_from_authority,
};
use crate::observation::{
BackendObservationProxy, ObservationProxyError, RuntimeObservationClient,
RuntimeObservationSource, RuntimeObservationSourceConfig,
};
use crate::profile_settings::{
CreateWorkspaceProfileSourceRequest, DeleteWorkspaceProfileSourceRequest,
DeleteWorkspaceProfileTreeFileRequest, ReadWorkspaceProfileTreeFileQuery,
UpdateWorkspaceMetadataRequest, UpdateWorkspaceProfileRegistryRequest,
UpdateWorkspaceProfileSourceRequest, WriteWorkspaceProfileTreeFileRequest,
};
use crate::records::{ObjectiveDetail, ProjectRecordList, TicketDetail};
use crate::repositories::{
ConfiguredRepository, RepositoryListProjection, RepositoryLogRead, RepositoryLookupError,
RepositoryRegistryReader, RepositorySummary,
};
use crate::resource_broker::BackendResourceBroker;
use crate::runtime_subscription::RuntimeSubscriptionBroker;
use crate::skills;
use crate::store::{
AccountRecord, ApiTokenRecord, AuthChallengeRecord, BrowserSessionRecord, ControlPlaneStore,
DeviceLoginFlowRecord, PasskeyCredentialRecord, RepositoryRecord, TicketWorkerAssignmentRecord,
UserRecord, WorkdirRegistryRecord, WorkerRegistryRecord, WorkerWorkdirLinkRecord,
WorkspaceRecord,
};
use crate::{Error, Result};
use worker_runtime::catalog::{
ConfigBundleRef, MaterializerKind, ProfileSelector,
RepositorySelector as RuntimeRepositorySelector, WorkingDirectoryClaim,
WorkingDirectoryOccupancy, WorkingDirectoryRepository, WorkingDirectoryRequest,
WorkingDirectoryStatusKind, WorkingDirectorySummary, WorkspaceApiRef,
};
use worker_runtime::config_bundle::ConfigBundle;
use worker_runtime::http_server::{
RuntimeHttpConfigBundleAvailabilityResponse, RuntimeHttpConfigBundlesResponse,
RuntimeHttpSummaryResponse, RuntimeHttpWorkerResponse, RuntimeHttpWorkersResponse,
};
use worker_runtime::identity::WorkerId;
use worker_runtime::interaction::{
WorkerInput as EmbeddedWorkerInput, WorkerInputKind as EmbeddedWorkerInputKind,
};
const EMBEDDED_WORKER_RUNTIME_ID: &str = "embedded-worker-runtime";
fn embedded_runtime_id() -> String {
EMBEDDED_WORKER_RUNTIME_ID.to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AuthConfig {
/// Browser human auth uses Passkey ceremonies and HttpOnly cookie sessions;
/// CLI/TUI auth uses API tokens obtained through the device login flow.
Passkey {
rp_id: String,
origin: String,
public_base_url: String,
cookie_name: String,
},
}
#[derive(Clone)]
pub struct ServerConfig {
pub workspace_id: String,
pub workspace_display_name: String,
pub workspace_created_at: String,
pub workspace_root: PathBuf,
pub database_path: PathBuf,
pub frontend_url: String,
pub embedded_runtime_store_root: PathBuf,
pub static_assets_dir: Option<PathBuf>,
pub auth: AuthConfig,
pub max_records: usize,
pub repositories: Vec<ConfiguredRepository>,
pub runtime_event_sources: Vec<RuntimeObservationSourceConfig>,
pub remote_runtime_sources: Vec<RemoteRuntimeConfig>,
pub runtime_config_path: Option<PathBuf>,
pub backend_base_url: Option<String>,
}
impl ServerConfig {
pub fn local_dev(workspace_root: impl Into<PathBuf>, identity: WorkspaceIdentity) -> Self {
let workspace_root = workspace_root.into();
let workspace_id = identity.workspace_id;
let embedded_runtime_store_root = Self::default_embedded_runtime_store_root(&workspace_id);
let database_path = Self::default_server_database_path();
Self {
workspace_id,
workspace_display_name: identity.display_name,
workspace_created_at: identity.created_at,
workspace_root,
database_path,
frontend_url: "http://127.0.0.1:5173".to_string(),
embedded_runtime_store_root,
static_assets_dir: None,
auth: AuthConfig::Passkey {
rp_id: "localhost".to_string(),
origin: "http://localhost:8787".to_string(),
public_base_url: "http://localhost:8787".to_string(),
cookie_name: "yoi_workspace_session".to_string(),
},
max_records: 200,
repositories: Vec::new(),
runtime_event_sources: Vec::new(),
remote_runtime_sources: Vec::new(),
runtime_config_path: BackendRuntimesConfigFile::default_path(),
backend_base_url: None,
}
}
pub fn server_data_root_for_data_dir(data_dir: impl Into<PathBuf>) -> PathBuf {
data_dir.into().join("server")
}
pub fn default_server_data_root() -> PathBuf {
match manifest::paths::data_dir() {
Some(data_dir) => Self::server_data_root_for_data_dir(data_dir),
None => std::env::temp_dir().join("yoi").join("server"),
}
}
pub fn server_database_path_for_data_dir(data_dir: impl Into<PathBuf>) -> PathBuf {
Self::server_data_root_for_data_dir(data_dir).join("server.db")
}
pub fn default_server_database_path() -> PathBuf {
Self::default_server_data_root().join("server.db")
}
pub fn workspace_backend_data_root_for_data_dir(
data_dir: impl Into<PathBuf>,
workspace_id: impl AsRef<str>,
) -> PathBuf {
Self::server_data_root_for_data_dir(data_dir)
.join("workspaces")
.join(workspace_id.as_ref())
}
pub fn default_workspace_backend_data_root(workspace_id: impl AsRef<str>) -> PathBuf {
match manifest::paths::data_dir() {
Some(data_dir) => {
Self::workspace_backend_data_root_for_data_dir(data_dir, workspace_id.as_ref())
}
None => std::env::temp_dir()
.join("yoi")
.join("server")
.join("workspaces")
.join(workspace_id.as_ref()),
}
}
pub fn embedded_runtime_store_root_for_data_dir(
data_dir: impl Into<PathBuf>,
workspace_id: impl AsRef<str>,
) -> PathBuf {
Self::workspace_backend_data_root_for_data_dir(data_dir, workspace_id)
.join("embedded-runtime")
}
pub fn default_embedded_runtime_store_root(workspace_id: impl AsRef<str>) -> PathBuf {
Self::default_workspace_backend_data_root(workspace_id).join("embedded-runtime")
}
pub fn with_embedded_runtime_store_root(mut self, root: impl Into<PathBuf>) -> Self {
self.embedded_runtime_store_root = root.into();
self
}
}
const ORCHESTRATOR_ATTENTION_TICKET_LIMIT: usize = 20;
const ORCHESTRATOR_ATTENTION_PROMPT: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../resources/prompts/internal/workspace_orchestrator_queue_attention.md"
));
#[derive(Clone)]
pub struct WorkspaceApi {
config: ServerConfig,
store: Arc<dyn ControlPlaneStore>,
authority: SqliteWorkspaceAuthority,
runtime: Arc<RuntimeRegistry>,
companion: Arc<CompanionConsole>,
orchestrator_spawn_lock: Arc<std::sync::Mutex<()>>,
orchestrator_attention_fingerprint: Arc<Mutex<Option<String>>>,
observation_proxy: BackendObservationProxy,
runtime_subscription_broker: RuntimeSubscriptionBroker,
resource_broker: BackendResourceBroker,
}
impl WorkspaceApi {
pub async fn new(config: ServerConfig, store: Arc<dyn ControlPlaneStore>) -> Result<Self> {
let resource_broker = BackendResourceBroker::default();
let execution_backend = WorkerRuntimeExecutionBackend::new(
ProfileRuntimeWorkerFactory::new(config.workspace_root.clone())
.with_resource_client(Arc::new(resource_broker.clone())),
)
.map_err(|err| {
crate::Error::Store(format!(
"failed to initialize embedded Worker backend: {err}"
))
})?;
Self::new_with_execution_backend_and_broker(
config,
store,
Arc::new(execution_backend),
resource_broker,
)
.await
}
#[cfg(test)]
async fn new_with_execution_backend(
config: ServerConfig,
store: Arc<dyn ControlPlaneStore>,
execution_backend: Arc<dyn worker_runtime::execution::WorkerExecutionBackend>,
) -> Result<Self> {
Self::new_with_execution_backend_and_broker(
config,
store,
execution_backend,
BackendResourceBroker::default(),
)
.await
}
async fn new_with_execution_backend_and_broker(
mut config: ServerConfig,
store: Arc<dyn ControlPlaneStore>,
execution_backend: Arc<dyn worker_runtime::execution::WorkerExecutionBackend>,
resource_broker: BackendResourceBroker,
) -> Result<Self> {
store
.upsert_workspace(&WorkspaceRecord {
workspace_id: config.workspace_id.clone(),
owner_account_id: None,
display_name: config.workspace_display_name.clone(),
state: "active".to_string(),
created_at: config.workspace_created_at.clone(),
updated_at: config.workspace_created_at.clone(),
})
.await?;
import_configured_repositories(store.as_ref(), &config)?;
config.repositories = load_configured_repositories_from_store(store.as_ref(), &config)?;
let embedded_runtime = EmbeddedWorkerRuntime::new_fs_store_with_execution_backend(
config.workspace_id.clone(),
config.embedded_runtime_store_root.clone(),
execution_backend,
)
.map(|runtime| runtime.with_resource_broker(resource_broker.clone()))
.map_err(|err| crate::Error::Store(format!("invalid embedded Worker backend: {err}")))?;
let embedded_subscription_runtime = embedded_runtime.subscription_runtime();
let embedded_runtime_id = EMBEDDED_WORKER_RUNTIME_ID.to_string();
let runtime = RuntimeRegistry::for_workspace(embedded_runtime);
let runtime_subscription_broker =
RuntimeSubscriptionBroker::new(config.workspace_id.clone());
runtime_subscription_broker
.register_embedded_runtime(embedded_runtime_id, embedded_subscription_runtime);
for remote_config in config.remote_runtime_sources.iter().cloned() {
let remote_runtime = RemoteWorkerRuntime::new(
remote_config.clone(),
config.workspace_id.clone(),
config
.backend_base_url
.clone()
.unwrap_or_else(|| "http://127.0.0.1:8787".to_string()),
)
.map(|host| host.with_resource_broker(resource_broker.clone()))
.map_err(|err| err.into_error())?;
runtime.register(remote_runtime);
runtime_subscription_broker.register_remote_runtime(remote_config);
}
let runtime = Arc::new(runtime);
let companion = Arc::new(CompanionConsole::disabled());
let observation_proxy = BackendObservationProxy::new(config.runtime_event_sources.clone());
Ok(Self {
authority: SqliteWorkspaceAuthority::new(
config.database_path.clone(),
config.workspace_id.clone(),
)?,
config,
store,
runtime,
companion,
orchestrator_spawn_lock: Arc::new(std::sync::Mutex::new(())),
orchestrator_attention_fingerprint: Arc::new(Mutex::new(None)),
observation_proxy,
runtime_subscription_broker,
resource_broker,
})
}
pub fn workspace_id(&self) -> &str {
self.config.workspace_id.as_str()
}
pub fn runtime_subscription_broker(&self) -> &RuntimeSubscriptionBroker {
&self.runtime_subscription_broker
}
fn workspace_api_ref(&self, runtime_id: &str) -> WorkspaceApiRef {
WorkspaceApiRef {
workspace_id: self.config.workspace_id.clone(),
base_url: self
.config
.backend_base_url
.clone()
.unwrap_or_else(|| "http://127.0.0.1:8787".to_string())
.trim_end_matches('/')
.to_string(),
runtime_id: Some(runtime_id.to_string()),
}
}
fn spawn_workspace_worker(
&self,
runtime_id: &str,
mut request: WorkerSpawnRequest,
) -> ApiResult<WorkerSpawnResult> {
let workspace_api = self.workspace_api_ref(runtime_id);
request.resolved_workspace_api = Some(workspace_api.clone());
let result = self
.runtime
.spawn_worker(runtime_id, request)
.map_err(|error| error.into_error())?;
let Some(worker) = result.worker.as_ref() else {
return Ok(result);
};
let replacement = match self.runtime.replace_worker_workspace_api(
runtime_id,
&worker.worker_id,
workspace_api,
) {
Ok(replacement) => replacement,
Err(error) => {
let _ = self.runtime.delete_worker(runtime_id, &worker.worker_id);
return Err(error.into_error().into());
}
};
if replacement.state != WorkerOperationState::Accepted {
let _ = self.runtime.delete_worker(runtime_id, &worker.worker_id);
return Err(Error::RuntimeOperationFailed {
runtime_id: runtime_id.to_string(),
code: "worker_workspace_api_replace_failed".to_string(),
message: replacement
.diagnostics
.first()
.map(|diagnostic| diagnostic.message.clone())
.unwrap_or_else(|| {
"Runtime rejected Workspace API replacement after spawn".to_string()
}),
}
.into());
}
Ok(result)
}
fn restore_workspace_worker(
&self,
runtime_id: &str,
worker_id: &str,
) -> ApiResult<WorkerRestoreResult> {
let binding = self
.runtime
.replace_worker_workspace_api(runtime_id, worker_id, self.workspace_api_ref(runtime_id))
.map_err(|error| error.into_error())?;
if binding.state != WorkerOperationState::Accepted {
return Ok(WorkerRestoreResult {
state: binding.state,
worker: binding.worker,
diagnostics: binding.diagnostics,
});
}
Ok(self
.runtime
.restore_worker(runtime_id, worker_id)
.map_err(|error| error.into_error())?)
}
fn repository_reader(&self) -> RepositoryRegistryReader {
RepositoryRegistryReader::new(self.config.repositories.clone())
}
}
fn import_configured_repositories(
store: &dyn ControlPlaneStore,
config: &ServerConfig,
) -> Result<()> {
if config.repositories.is_empty() {
return Ok(());
}
let now = crate::auth::now_rfc3339();
for repository in &config.repositories {
store.upsert_repository(&RepositoryRecord {
workspace_id: config.workspace_id.clone(),
repository_id: repository.id.clone(),
name: repository
.display_name
.clone()
.unwrap_or_else(|| repository.id.clone()),
kind: repository.provider.clone(),
provider: Some(repository.provider.clone()),
uri: repository.uri.clone(),
default_ref: repository.default_selector.clone(),
auth_ref_kind: None,
auth_ref_key: None,
created_at: now.clone(),
updated_at: now.clone(),
})?;
}
Ok(())
}
fn load_configured_repositories_from_store(
store: &dyn ControlPlaneStore,
config: &ServerConfig,
) -> Result<Vec<ConfiguredRepository>> {
store
.list_repositories(&config.workspace_id)?
.into_iter()
.map(|record| configured_repository_from_record(&config.workspace_root, record))
.collect()
}
fn configured_repository_from_record(
workspace_root: &Path,
record: RepositoryRecord,
) -> Result<ConfiguredRepository> {
let provider = record.provider.unwrap_or_else(|| record.kind.clone());
if record.uri.contains("://") {
return Err(Error::Config(format!(
"repository `{}` uses a remote URI, but remote repository materialization is not implemented",
record.repository_id
)));
}
let path = resolve_backend_path(workspace_root, Path::new(&record.uri));
Ok(ConfiguredRepository {
id: record.repository_id,
provider,
uri: record.uri,
path,
display_name: Some(record.name),
default_selector: record.default_ref,
})
}
fn resolve_backend_path(workspace_root: &Path, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
workspace_root.join(path)
}
}
pub fn build_router(api: WorkspaceApi) -> Router {
Router::new()
.route("/api/auth/config", get(get_auth_config))
.route("/api/auth/bootstrap-user", post(post_auth_bootstrap_user))
.route(
"/api/auth/passkeys/registration/options",
post(post_passkey_registration_options),
)
.route(
"/api/auth/passkeys/registration/complete",
post(post_passkey_registration_complete),
)
.route(
"/api/auth/passkeys/login/options",
post(post_passkey_login_options),
)
.route(
"/api/auth/passkeys/login/complete",
post(post_passkey_login_complete),
)
.route("/api/auth/logout", post(post_auth_logout))
.route("/api/auth/device-login/start", post(post_device_login_start))
.route("/api/auth/device-login/approve", post(post_device_login_approve))
.route("/api/auth/device-login/poll", post(post_device_login_poll))
.route("/api/auth/whoami", get(get_auth_whoami))
.route("/api/workspace", get(get_workspace))
.route("/api/w/{workspace_id}/workspace", get(scoped_get_workspace))
.route(
"/api/w/{workspace_id}/settings/workspace",
get(scoped_get_workspace_settings).put(scoped_update_workspace_settings),
)
.route(
"/api/w/{workspace_id}/settings/profiles",
get(scoped_get_profile_settings).post(scoped_create_profile_source),
)
.route(
"/api/w/{workspace_id}/settings/profiles/registry",
put(scoped_update_profile_registry),
)
.route(
"/api/w/{workspace_id}/settings/profiles/trees/{source_tree_id}",
get(scoped_get_profile_source_tree),
)
.route(
"/api/w/{workspace_id}/settings/profiles/trees/{source_tree_id}/file",
get(scoped_get_profile_tree_file)
.put(scoped_write_profile_tree_file)
.delete(scoped_delete_profile_tree_file),
)
.route(
"/api/w/{workspace_id}/settings/profiles/{profile_source_id}",
get(scoped_get_profile_source)
.put(scoped_update_profile_source)
.delete(scoped_delete_profile_source),
)
.route("/api/tickets", get(list_tickets))
.route(
"/api/w/{workspace_id}/tickets",
get(scoped_list_tickets).post(scoped_create_ticket_record),
)
.route(
"/api/w/{workspace_id}/memory",
get(scoped_get_memory_document),
)
.route(
"/api/w/{workspace_id}/memory/staging",
get(scoped_list_memory_staging),
)
.route(
"/api/w/{workspace_id}/memory/backend",
post(scoped_memory_backend_operation),
)
.route(
"/api/w/{workspace_id}/memory/consolidation",
post(scoped_memory_consolidation),
)
.route("/api/tickets/{id}", get(get_ticket))
.route("/api/w/{workspace_id}/skills", get(scoped_list_skills))
.route("/api/w/{workspace_id}/skills/lint", get(scoped_lint_skills))
.route("/api/w/{workspace_id}/skills/{name}", get(scoped_get_skill))
.route(
"/api/w/{workspace_id}/skills/{name}/activate",
get(scoped_activate_skill),
)
.route(
"/api/w/{workspace_id}/tickets/default-intake-ready-body",
post(scoped_default_intake_ready_body),
)
.route(
"/api/w/{workspace_id}/tickets/search",
get(scoped_list_ticket_summaries),
)
.route(
"/api/w/{workspace_id}/tickets/doctor",
get(scoped_ticket_doctor),
)
.route(
"/api/w/{workspace_id}/tickets/relations/search",
post(scoped_query_ticket_relations),
)
.route(
"/api/w/{workspace_id}/tickets/orchestration-plans/search",
post(scoped_query_ticket_orchestration_plans),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/record",
get(scoped_get_ticket_record),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/item",
patch(scoped_edit_ticket_record_item),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/dependency-check",
get(scoped_ticket_dependency_check),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/thread-events",
post(scoped_add_ticket_thread_event),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/state-changes",
post(scoped_add_ticket_state_change),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/intake-summaries",
post(scoped_add_ticket_intake_summary),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/state-fields/{field}",
post(scoped_set_ticket_state_field),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/workflow-state",
post(scoped_set_ticket_workflow_state),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/intake-ready",
post(scoped_prepare_ticket_intake_ready),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/workflow/queue",
post(scoped_queue_ticket_record),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/workflow/review",
post(scoped_review_ticket_record),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/workflow/close",
post(scoped_close_ticket_record),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/relation-view",
get(scoped_ticket_relation_view),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/relations",
post(scoped_record_ticket_relation),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/orchestration-plans",
post(scoped_record_ticket_orchestration_plan),
)
.route(
"/api/w/{workspace_id}/tickets/{id}",
get(scoped_get_ticket).patch(scoped_edit_ticket_item),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/assignment",
get(scoped_get_ticket_worker_assignment)
.put(scoped_set_ticket_worker_assignment)
.delete(scoped_clear_ticket_worker_assignment),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/assignment/reassign",
post(scoped_reassign_ticket_worker_assignment),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/state",
post(scoped_transition_ticket_state),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/events",
post(scoped_append_ticket_event),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/reviews",
post(scoped_review_ticket),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/queue",
post(scoped_queue_ticket),
)
.route(
"/api/w/{workspace_id}/tickets/{id}/close",
post(scoped_close_ticket),
)
.route("/api/objectives", get(list_objectives))
.route(
"/api/w/{workspace_id}/objectives",
get(scoped_list_objectives).post(scoped_create_objective),
)
.route("/api/objectives/{id}", get(get_objective))
.route(
"/api/w/{workspace_id}/objectives/{objective_id}",
get(scoped_get_objective).patch(scoped_edit_objective),
)
.route(
"/api/w/{workspace_id}/objectives/{objective_id}/state",
post(scoped_set_objective_state),
)
.route(
"/api/w/{workspace_id}/objectives/{objective_id}/ticket-links",
post(scoped_link_objective_ticket),
)
.route(
"/api/w/{workspace_id}/objectives/{objective_id}/ticket-links/{ticket_id}",
delete(scoped_unlink_objective_ticket),
)
.route("/api/repositories", get(list_repositories))
.route(
"/api/w/{workspace_id}/repositories",
get(scoped_list_repositories),
)
.route("/api/repositories/{repository_id}", get(repository_detail))
.route(
"/api/w/{workspace_id}/repositories/{repository_id}",
get(scoped_repository_detail),
)
.route("/api/repositories/{repository_id}/log", get(repository_log))
.route(
"/api/w/{workspace_id}/repositories/{repository_id}/log",
get(scoped_repository_log),
)
.route("/api/hosts", get(list_hosts))
.route("/api/w/{workspace_id}/hosts", get(scoped_list_hosts))
.route(
"/api/w/{workspace_id}/profile-source-archives/{digest}",
get(scoped_get_profile_source_archive),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories",
get(scoped_list_runtime_working_directories).post(scoped_create_runtime_working_directory),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories/{working_directory_id}",
get(scoped_runtime_working_directory_detail).delete(scoped_cleanup_runtime_working_directory),
)
.route(
"/api/w/{workspace_id}/working-directories",
get(scoped_list_working_directories).post(scoped_create_working_directory),
)
.route(
"/api/w/{workspace_id}/working-directories/{working_directory_id}",
get(scoped_working_directory_detail).delete(scoped_cleanup_working_directory),
)
.route("/api/runtimes", get(list_runtimes))
.route("/api/w/{workspace_id}/runtimes", get(scoped_list_runtimes))
.route(
"/api/workers",
get(list_workers).post(create_workspace_worker),
)
.route(
"/api/w/{workspace_id}/orchestrator",
get(scoped_workspace_orchestrator_status)
.post(scoped_start_workspace_orchestrator),
)
.route(
"/api/w/{workspace_id}/workers",
get(scoped_list_workers).post(scoped_create_workspace_worker),
)
.route(
"/api/w/{workspace_id}/protocol/ws",
get(scoped_workspace_protocol_ws),
)
.route(
"/api/workers/launch-options",
get(get_worker_launch_options),
)
.route(
"/api/w/{workspace_id}/workers/launch-options",
get(scoped_get_worker_launch_options),
)
.route(
"/api/settings/runtime-connections",
get(get_runtime_connection_settings),
)
.route(
"/api/w/{workspace_id}/settings/runtime-connections",
get(scoped_get_runtime_connection_settings),
)
.route(
"/api/settings/runtime-connections/remotes",
post(add_remote_runtime_connection),
)
.route(
"/api/w/{workspace_id}/settings/runtime-connections/remotes",
post(scoped_add_remote_runtime_connection),
)
.route(
"/api/settings/runtime-connections/remotes/{runtime_id}",
delete(delete_remote_runtime_connection),
)
.route(
"/api/w/{workspace_id}/settings/runtime-connections/remotes/{runtime_id}",
delete(scoped_delete_remote_runtime_connection),
)
.route(
"/api/settings/runtime-connections/remotes/{runtime_id}/test",
post(test_remote_runtime_connection),
)
.route(
"/api/w/{workspace_id}/settings/runtime-connections/remotes/{runtime_id}/test",
post(scoped_test_remote_runtime_connection),
)
.route(
"/internal/runtime/resources/fetch",
post(post_internal_runtime_resource_fetch),
)
.route("/api/companion/status", get(get_companion_status))
.route(
"/api/w/{workspace_id}/companion/status",
get(scoped_get_companion_status),
)
.route("/api/companion/transcript", get(get_companion_transcript))
.route(
"/api/w/{workspace_id}/companion/transcript",
get(scoped_get_companion_transcript),
)
.route("/api/companion/messages", post(post_companion_message))
.route(
"/api/w/{workspace_id}/companion/messages",
post(scoped_post_companion_message),
)
.route("/api/companion/cancel", post(post_companion_cancel))
.route(
"/api/w/{workspace_id}/companion/cancel",
post(scoped_post_companion_cancel),
)
.route(
"/api/runtimes/{runtime_id}/workers",
get(list_runtime_workers).post(create_runtime_worker),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers",
get(scoped_list_runtime_workers).post(scoped_create_runtime_worker),
)
.route(
"/api/runtimes/{runtime_id}/config-bundles",
post(sync_runtime_config_bundle),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/config-bundles",
post(scoped_sync_runtime_config_bundle),
)
.route(
"/api/runtimes/{runtime_id}/config-bundles/{bundle_id}/availability",
get(check_runtime_config_bundle),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/config-bundles/{bundle_id}/availability",
get(scoped_check_runtime_config_bundle),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}",
get(get_runtime_worker),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}/restore",
post(restore_runtime_worker),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}",
get(scoped_get_runtime_worker),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/restore",
post(scoped_restore_runtime_worker),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/pin",
put(scoped_pin_runtime_worker).delete(scoped_unpin_runtime_worker),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/cleanup-plan",
get(scoped_runtime_cleanup_plan),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/cleanup-executions",
post(scoped_execute_runtime_cleanup),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}/input",
post(send_runtime_worker_input),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/input",
post(scoped_send_runtime_worker_input),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}/completions",
post(runtime_worker_completions),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/completions",
post(scoped_runtime_worker_completions),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}/stop",
post(stop_runtime_worker),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/stop",
post(scoped_stop_runtime_worker),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}/cancel",
post(cancel_runtime_worker),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/cancel",
post(scoped_cancel_runtime_worker),
)
.route(
"/api/runtimes/{runtime_id}/workers/{worker_id}/protocol/ws",
get(worker_protocol_ws),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/protocol/ws",
get(scoped_worker_protocol_ws),
)
.route("/api/hosts/{host_id}/workers", get(list_host_workers))
.route(
"/api/w/{workspace_id}/hosts/{host_id}/workers",
get(scoped_list_host_workers),
)
.fallback(get(static_or_spa_fallback))
.with_state(api)
}
pub async fn serve(
config: ServerConfig,
store: Arc<dyn ControlPlaneStore>,
listener: TcpListener,
) -> Result<()> {
let api = WorkspaceApi::new(config, store).await?;
let orchestrator_hook = tokio::spawn(run_orchestrator_turn_end_hook(api.clone()));
let result = axum::serve(listener, build_router(api)).await;
orchestrator_hook.abort();
result?;
Ok(())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WorkspaceResponse {
pub workspace_id: String,
pub display_name: String,
pub record_authority: String,
pub schema_version: i64,
pub auth: AuthConfig,
pub extension_points: ExtensionPoints,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ExtensionPoints {
pub store: String,
pub event_stream: ExtensionPointState,
pub host_worker_bridge: ExtensionPointState,
pub companion_console: ExtensionPointState,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ExtensionPointState {
pub status: String,
pub note: String,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ListResponse<T> {
pub workspace_id: String,
pub limit: usize,
pub items: Vec<T>,
pub invalid_records: Vec<crate::records::InvalidProjectRecord>,
pub record_authority: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RuntimeListResponse<T> {
pub workspace_id: String,
pub limit: usize,
pub items: Vec<T>,
pub source: String,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Default, Deserialize)]
struct RuntimeWorkersQuery {
status: Option<RuntimeWorkersStatusFilter>,
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
enum RuntimeWorkersStatusFilter {
Stopped,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WorkerRestoreResponse {
pub workspace_id: String,
pub runtime_id: String,
pub worker_id: String,
pub result: WorkerRestoreResult,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CleanupTargetKind {
WorkerDelete,
WorkdirCleanCleanup,
WorkdirDirtyDiscard,
WorkdirRecordDelete,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CleanupWorkdirFileStatus {
Pending,
Present,
Active,
CleanupPending,
NotFound,
Corrupted,
Failed,
Unknown,
}
impl CleanupWorkdirFileStatus {
fn from_registry(value: &str) -> Self {
match value {
"pending" => Self::Pending,
"present" => Self::Present,
"active" => Self::Active,
"cleanup_pending" => Self::CleanupPending,
"not_found" => Self::NotFound,
"corrupted" => Self::Corrupted,
"failed" => Self::Failed,
"unknown" => Self::Unknown,
_ => Self::Unknown,
}
}
fn from_runtime(value: &WorkingDirectoryStatusKind) -> Self {
match value {
WorkingDirectoryStatusKind::Active => Self::Active,
WorkingDirectoryStatusKind::CleanupPending => Self::CleanupPending,
WorkingDirectoryStatusKind::Corrupted => Self::Corrupted,
WorkingDirectoryStatusKind::NotFound => Self::NotFound,
WorkingDirectoryStatusKind::Unknown => Self::Unknown,
}
}
fn is_record_only(self) -> bool {
matches!(self, Self::NotFound)
}
fn is_corrupted(self) -> bool {
matches!(self, Self::Corrupted)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CleanupWorkdirCleanliness {
Clean,
Dirty,
Unknown,
}
impl CleanupWorkdirCleanliness {
fn from_registry(value: &str) -> Self {
match value {
"clean" => Self::Clean,
"dirty" => Self::Dirty,
"unknown" => Self::Unknown,
_ => Self::Unknown,
}
}
fn from_runtime(value: Option<&str>) -> Self {
value.map(Self::from_registry).unwrap_or(Self::Unknown)
}
fn is_clean(self) -> bool {
matches!(self, Self::Clean)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CleanupWorkerCandidate {
pub target_id: String,
pub action: CleanupTargetKind,
pub worker_id: String,
pub runtime_worker_id: String,
pub runtime_id: String,
pub reason: String,
pub blocking_reason: Option<String>,
pub pinned: bool,
pub retention_state: String,
pub linked_workdir_ids: Vec<String>,
pub running_linked: bool,
pub estimated_reclaim_bytes: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CleanupWorkdirCandidate {
pub target_id: String,
pub action: CleanupTargetKind,
pub workdir_id: String,
pub runtime_id: String,
pub repository_id: String,
pub reason: String,
pub blocking_reason: Option<String>,
pub linked_worker_ids: Vec<String>,
pub linked_running_worker_ids: Vec<String>,
pub running_linked: bool,
pub pinned_linked: bool,
pub file_status: CleanupWorkdirFileStatus,
pub cleanliness: CleanupWorkdirCleanliness,
pub estimated_reclaim_bytes: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeCleanupPlanResponse {
pub workspace_id: String,
pub runtime_id: String,
pub generated_at: String,
pub revision: String,
pub digest: String,
pub workers: Vec<CleanupWorkerCandidate>,
pub workdirs: Vec<CleanupWorkdirCandidate>,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExecuteRuntimeCleanupRequest {
pub expected_plan_revision: String,
pub expected_plan_digest: String,
#[serde(default)]
pub worker_target_ids: Vec<String>,
#[serde(default)]
pub workdir_target_ids: Vec<String>,
#[serde(default)]
pub confirm_dirty_discard_target_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeCleanupExecutionResult {
pub target_id: String,
pub action: CleanupTargetKind,
pub status: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeCleanupExecutionResponse {
pub workspace_id: String,
pub runtime_id: String,
pub executed_at: String,
pub results: Vec<RuntimeCleanupExecutionResult>,
pub plan_after: RuntimeCleanupPlanResponse,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerRetentionResponse {
pub workspace_id: String,
pub runtime_id: String,
pub worker_id: String,
pub pinned: bool,
pub retention_state: String,
}
#[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)]
#[serde(deny_unknown_fields)]
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 default_profile: Option<String>,
pub profiles: Vec<WorkerLaunchProfileCandidate>,
pub repositories: Vec<WorkingDirectoryRepositoryOption>,
pub working_directories: Vec<WorkingDirectorySummary>,
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 working_directory_required: 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, Clone, Serialize, Deserialize)]
pub struct WorkingDirectoryRepositoryOption {
pub id: String,
pub display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_selector: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BrowserWorkingDirectoryCreateRequest {
#[serde(default = "embedded_runtime_id")]
pub runtime_id: String,
pub repository_id: String,
#[serde(default)]
pub selector: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BrowserWorkingDirectoryListResponse {
pub workspace_id: String,
pub items: Vec<WorkingDirectorySummary>,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BrowserWorkingDirectoryDetailResponse {
pub workspace_id: String,
pub item: WorkingDirectorySummary,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BrowserWorkerWorkingDirectorySelection {
pub working_directory_id: String,
#[serde(default)]
pub relative_cwd: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BrowserWorkspaceOrchestratorResponse {
pub workspace_id: String,
pub online: bool,
pub disposition: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub worker: Option<WorkerSummary>,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BrowserCreateWorkerRequest {
pub runtime_id: String,
pub display_name: String,
#[serde(default)]
pub profile: Option<String>,
pub initial_text: String,
#[serde(default)]
pub working_directory: Option<BrowserWorkerWorkingDirectorySelection>,
}
#[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,
pub items: Vec<RepositorySummary>,
pub source: String,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RepositoryDetailResponse {
pub workspace_id: String,
pub item: RepositorySummary,
pub source: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RepositoryLogResponse {
pub workspace_id: String,
pub repository_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_selector: Option<String>,
pub limit: usize,
pub items: Vec<crate::repositories::GitCommitSummary>,
pub diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Debug, Deserialize)]
struct LogQuery {
limit: Option<usize>,
}
#[derive(Debug, Deserialize)]
struct MemoryStagingQuery {
limit: Option<usize>,
}
#[derive(Debug, Deserialize)]
struct ObjectiveListQuery {
limit: Option<usize>,
}
#[derive(Debug, Deserialize)]
struct ObjectiveCreateRequest {
title: String,
#[serde(default)]
body_md: String,
#[serde(default = "default_objective_state")]
state: String,
#[serde(default)]
linked_tickets: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct ObjectiveEditRequest {
title: Option<String>,
old_string: Option<String>,
new_string: Option<String>,
#[serde(default)]
replace_all: bool,
}
#[derive(Debug, Deserialize)]
struct TicketListQuery {
limit: Option<usize>,
}
#[derive(Debug, Deserialize)]
struct ObjectiveStateRequest {
state: String,
}
#[derive(Debug, Deserialize)]
struct ObjectiveLinkTicketRequest {
ticket_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedObjectivePath {
workspace_id: String,
objective_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedObjectiveTicketPath {
workspace_id: String,
objective_id: String,
ticket_id: String,
}
fn default_objective_state() -> String {
"active".to_string()
}
#[derive(Debug, Deserialize)]
struct TranscriptQuery {
start: Option<usize>,
limit: Option<usize>,
}
#[derive(Debug, Deserialize)]
struct ScopedWorkspacePath {
workspace_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedRecordPath {
workspace_id: String,
id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedSkillPath {
workspace_id: String,
name: String,
}
#[derive(Debug, Deserialize)]
struct ScopedRepositoryPath {
workspace_id: String,
repository_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedProfileArchivePath {
workspace_id: String,
digest: String,
}
#[derive(Debug, Deserialize)]
struct ScopedHostPath {
workspace_id: String,
host_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedRuntimePath {
workspace_id: String,
runtime_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedWorkingDirectoryPath {
workspace_id: String,
working_directory_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedRuntimeWorkingDirectoryPath {
workspace_id: String,
runtime_id: String,
working_directory_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedConfigBundlePath {
workspace_id: String,
runtime_id: String,
bundle_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedRuntimeWorkerPath {
workspace_id: String,
runtime_id: String,
worker_id: String,
}
fn validate_workspace_scope(api: &WorkspaceApi, workspace_id: &str) -> ApiResult<()> {
if workspace_id == api.workspace_id() {
Ok(())
} else {
Err(workspace_id_mismatch_error())
}
}
async fn scoped_get_workspace(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<WorkspaceResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_workspace(State(api)).await
}
async fn scoped_get_workspace_settings(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<crate::profile_settings::WorkspaceMetadataSettingsResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(crate::profile_settings::workspace_metadata_settings(
&api.config.workspace_root,
&api.config.workspace_id,
&api.config.workspace_created_at,
&api.config.workspace_display_name,
)))
}
async fn scoped_update_workspace_settings(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(request): Json<UpdateWorkspaceMetadataRequest>,
) -> ApiResult<Json<crate::profile_settings::WorkspaceMetadataMutationResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let workspace =
crate::profile_settings::update_workspace_metadata(&api.config.workspace_root, request)?;
Ok(Json(
crate::profile_settings::WorkspaceMetadataMutationResponse {
workspace,
diagnostics: vec![RuntimeDiagnostic {
code: "workspace_metadata_updated".to_string(),
severity: DiagnosticSeverity::Info,
message: "Workspace display metadata was updated.".to_string(),
}],
},
))
}
async fn scoped_get_profile_settings(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<crate::profile_settings::ProfileSettingsResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(crate::profile_settings::load_profile_settings(
&api.config.workspace_id,
&api.config.workspace_root,
)))
}
async fn scoped_create_profile_source(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(request): Json<CreateWorkspaceProfileSourceRequest>,
) -> ApiResult<Json<crate::profile_settings::ProfileSettingsMutationResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(crate::profile_settings::create_profile_source(
&api.config.workspace_id,
&api.config.workspace_root,
request,
)?))
}
async fn scoped_update_profile_registry(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(request): Json<UpdateWorkspaceProfileRegistryRequest>,
) -> ApiResult<Json<crate::profile_settings::ProfileSettingsMutationResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(crate::profile_settings::update_profile_registry(
&api.config.workspace_id,
&api.config.workspace_root,
request,
)?))
}
async fn scoped_get_profile_source_tree(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, source_tree_id)): AxumPath<(String, String)>,
) -> ApiResult<Json<crate::profile_settings::WorkspaceProfileSourceTreeResponse>> {
validate_workspace_scope(&api, &workspace_id)?;
Ok(Json(crate::profile_settings::read_profile_source_tree(
&workspace_id,
&api.config.workspace_root,
&source_tree_id,
)?))
}
async fn scoped_get_profile_tree_file(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, source_tree_id)): AxumPath<(String, String)>,
Query(query): Query<ReadWorkspaceProfileTreeFileQuery>,
) -> ApiResult<Json<crate::profile_settings::WorkspaceProfileSourceTreeFileResponse>> {
validate_workspace_scope(&api, &workspace_id)?;
Ok(Json(crate::profile_settings::read_profile_tree_file(
&workspace_id,
&api.config.workspace_root,
&source_tree_id,
query,
)?))
}
async fn scoped_write_profile_tree_file(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, source_tree_id)): AxumPath<(String, String)>,
Json(request): Json<WriteWorkspaceProfileTreeFileRequest>,
) -> ApiResult<Json<crate::profile_settings::WorkspaceProfileSourceTreeFileResponse>> {
validate_workspace_scope(&api, &workspace_id)?;
Ok(Json(crate::profile_settings::write_profile_tree_file(
&workspace_id,
&api.config.workspace_root,
&source_tree_id,
request,
)?))
}
async fn scoped_delete_profile_tree_file(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, source_tree_id)): AxumPath<(String, String)>,
Json(request): Json<DeleteWorkspaceProfileTreeFileRequest>,
) -> ApiResult<Json<crate::profile_settings::WorkspaceProfileSourceTreeResponse>> {
validate_workspace_scope(&api, &workspace_id)?;
Ok(Json(crate::profile_settings::delete_profile_tree_file(
&workspace_id,
&api.config.workspace_root,
&source_tree_id,
request,
)?))
}
async fn scoped_get_profile_source(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, profile_source_id)): AxumPath<(String, String)>,
) -> ApiResult<Json<crate::profile_settings::WorkspaceProfileSourceDetailResponse>> {
validate_workspace_scope(&api, &workspace_id)?;
Ok(Json(crate::profile_settings::read_profile_source(
&api.config.workspace_id,
&api.config.workspace_root,
&profile_source_id,
)?))
}
async fn scoped_update_profile_source(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, profile_source_id)): AxumPath<(String, String)>,
Json(request): Json<UpdateWorkspaceProfileSourceRequest>,
) -> ApiResult<Json<crate::profile_settings::ProfileSettingsMutationResponse>> {
validate_workspace_scope(&api, &workspace_id)?;
Ok(Json(crate::profile_settings::update_profile_source(
&api.config.workspace_id,
&api.config.workspace_root,
&profile_source_id,
request,
)?))
}
async fn scoped_delete_profile_source(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, profile_source_id)): AxumPath<(String, String)>,
Json(request): Json<DeleteWorkspaceProfileSourceRequest>,
) -> ApiResult<Json<crate::profile_settings::ProfileSettingsMutationResponse>> {
validate_workspace_scope(&api, &workspace_id)?;
Ok(Json(crate::profile_settings::delete_profile_source(
&api.config.workspace_id,
&api.config.workspace_root,
&profile_source_id,
request,
)?))
}
async fn scoped_list_tickets(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Query(query): Query<TicketListQuery>,
) -> ApiResult<Json<crate::records::TicketListResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_tickets(State(api), Query(query)).await
}
async fn scoped_get_ticket(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
) -> ApiResult<Json<TicketDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_ticket(State(api), AxumPath(path.id)).await
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct TicketWorkerAssignmentResponse {
workspace_id: String,
ticket_id: String,
assignment: Option<TicketWorkerAssignmentRecord>,
worker: Option<WorkerSummary>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct TicketWorkerAssignmentMutationResponse {
workspace_id: String,
ticket_id: String,
assignment: Option<TicketWorkerAssignmentRecord>,
previous_assignment_id: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct SetTicketWorkerAssignmentRequest {
operation_id: String,
runtime_id: String,
worker_id: String,
expected_assignment_id: Option<String>,
assigned_by: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct ClearTicketWorkerAssignmentQuery {
operation_id: Option<String>,
expected_assignment_id: Option<String>,
actor: Option<String>,
}
async fn scoped_get_ticket_worker_assignment(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
) -> ApiResult<Json<TicketWorkerAssignmentResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let ticket = api.authority.ticket(&path.id)?;
let assignment = api
.store
.get_current_ticket_worker_assignment(&path.workspace_id, &ticket.id)?;
let worker = assignment.as_ref().and_then(|assignment| {
api.runtime
.worker(&assignment.runtime_id, &assignment.worker_id)
.ok()
});
Ok(Json(TicketWorkerAssignmentResponse {
workspace_id: path.workspace_id,
ticket_id: ticket.id,
assignment,
worker,
}))
}
async fn scoped_set_ticket_worker_assignment(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
Json(request): Json<SetTicketWorkerAssignmentRequest>,
) -> ApiResult<Json<TicketWorkerAssignmentMutationResponse>> {
set_ticket_worker_assignment(api, path, request, false).await
}
async fn scoped_reassign_ticket_worker_assignment(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
Json(request): Json<SetTicketWorkerAssignmentRequest>,
) -> ApiResult<Json<TicketWorkerAssignmentMutationResponse>> {
set_ticket_worker_assignment(api, path, request, true).await
}
async fn set_ticket_worker_assignment(
api: WorkspaceApi,
path: ScopedRecordPath,
request: SetTicketWorkerAssignmentRequest,
allow_reassign: bool,
) -> ApiResult<Json<TicketWorkerAssignmentMutationResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let ticket = api.authority.ticket(&path.id)?;
let operation_id = require_ticket_assignment_value("operation_id", request.operation_id)?;
let runtime_id = require_ticket_assignment_value("runtime_id", request.runtime_id)?;
let worker_id = require_ticket_assignment_value("worker_id", request.worker_id)?;
let expected_assignment_id = request
.expected_assignment_id
.map(|value| require_ticket_assignment_value("expected_assignment_id", value))
.transpose()?;
let assigned_by = request
.assigned_by
.map(|value| require_ticket_assignment_value("assigned_by", value))
.transpose()?
.unwrap_or_else(|| "workspace-api".to_string());
let worker = api
.runtime
.worker(&runtime_id, &worker_id)
.map_err(|err| err.into_error())?;
let assigned_at = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
let record = TicketWorkerAssignmentRecord {
workspace_id: path.workspace_id.clone(),
ticket_id: ticket.id.clone(),
assignment_id: new_id("tasg"),
runtime_id: worker.runtime_id,
worker_id: worker.worker_id,
assigned_by,
assigned_at,
};
let update = api.store.set_current_ticket_worker_assignment(
&record,
expected_assignment_id.as_deref(),
&new_id("tasev"),
&operation_id,
allow_reassign,
)?;
Ok(Json(TicketWorkerAssignmentMutationResponse {
workspace_id: path.workspace_id,
ticket_id: ticket.id,
assignment: Some(update.current),
previous_assignment_id: update.previous.map(|assignment| assignment.assignment_id),
}))
}
async fn scoped_clear_ticket_worker_assignment(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
Query(query): Query<ClearTicketWorkerAssignmentQuery>,
) -> ApiResult<Json<TicketWorkerAssignmentMutationResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let ticket = api.authority.ticket(&path.id)?;
let operation_id = query
.operation_id
.map(|value| require_ticket_assignment_value("operation_id", value))
.transpose()?
.ok_or_else(|| {
Error::TicketAssignmentConflict("unassign requires operation_id".to_string())
})?;
let expected_assignment_id = query
.expected_assignment_id
.map(|value| require_ticket_assignment_value("expected_assignment_id", value))
.transpose()?;
let actor = query
.actor
.map(|value| require_ticket_assignment_value("actor", value))
.transpose()?
.unwrap_or_else(|| "workspace-api".to_string());
let previous = api.store.clear_current_ticket_worker_assignment(
&path.workspace_id,
&ticket.id,
expected_assignment_id.as_deref(),
&operation_id,
&new_id("tasev"),
&actor,
&Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
)?;
Ok(Json(TicketWorkerAssignmentMutationResponse {
workspace_id: path.workspace_id,
ticket_id: ticket.id,
assignment: None,
previous_assignment_id: previous.map(|assignment| assignment.assignment_id),
}))
}
fn assign_ticket_worker_from_lifecycle(
api: &WorkspaceApi,
assignment: &crate::hosts::WorkerTicketAssignmentRequest,
runtime_id: &str,
worker_id: &str,
) -> Result<TicketWorkerAssignmentRecord> {
let ticket = api.authority.ticket(&assignment.ticket_id)?;
let assigned_at = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
let record = TicketWorkerAssignmentRecord {
workspace_id: api.config.workspace_id.clone(),
ticket_id: ticket.id,
assignment_id: new_id("tasg"),
runtime_id: runtime_id.to_string(),
worker_id: worker_id.to_string(),
assigned_by: "worker-lifecycle".to_string(),
assigned_at,
};
Ok(api
.store
.set_current_ticket_worker_assignment(
&record,
None,
&new_id("tasev"),
&assignment.operation_id,
false,
)?
.current)
}
fn existing_lifecycle_assignment_worker(
api: &WorkspaceApi,
assignment: &crate::hosts::WorkerTicketAssignmentRequest,
runtime_id: &str,
) -> Result<Option<WorkerSummary>> {
let Some(operation) = api
.store
.get_ticket_assignment_operation(&api.config.workspace_id, &assignment.operation_id)?
else {
return Ok(None);
};
if operation.action != "assign"
|| operation.ticket_id != assignment.ticket_id
|| operation.runtime_id.as_deref() != Some(runtime_id)
{
return Err(Error::TicketAssignmentConflict(format!(
"assignment operation {} was already used with different lifecycle input",
assignment.operation_id
)));
}
let Some(worker_id) = operation.worker_id else {
return Ok(None);
};
let worker = api
.runtime
.worker(runtime_id, &worker_id)
.map_err(|error| error.into_error())?;
if operation.assignment_id.is_none() && worker.state == "stopped" {
return Ok(None);
}
Ok(Some(worker))
}
fn require_ticket_assignment_value(field: &str, value: String) -> Result<String> {
let value = value.trim();
if value.is_empty() {
return Err(Error::RuntimeOperationFailed {
runtime_id: "workspace-server".to_string(),
code: "invalid_ticket_assignment".to_string(),
message: format!("{field} must not be empty"),
});
}
Ok(value.to_string())
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct BrowserEditTicketRequest {
title: Option<String>,
body: Option<String>,
old_string: Option<String>,
new_string: Option<String>,
#[serde(default)]
replace_all: bool,
target: Option<TicketTargetEdit>,
author: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct BrowserTransitionTicketStateRequest {
state: TicketWorkflowState,
reason: Option<String>,
body: Option<String>,
author: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
enum BrowserTicketThreadRole {
Comment,
Plan,
Decision,
ImplementationReport,
}
impl From<BrowserTicketThreadRole> for TicketEventKind {
fn from(role: BrowserTicketThreadRole) -> Self {
match role {
BrowserTicketThreadRole::Comment => Self::Comment,
BrowserTicketThreadRole::Plan => Self::Plan,
BrowserTicketThreadRole::Decision => Self::Decision,
BrowserTicketThreadRole::ImplementationReport => Self::ImplementationReport,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct BrowserAppendTicketEventRequest {
role: BrowserTicketThreadRole,
body: String,
author: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct BrowserReviewTicketRequest {
result: TicketReviewResult,
body: String,
author: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct BrowserQueueTicketRequest {
queued_by: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct BrowserCloseTicketRequest {
resolution: String,
}
fn browser_ticket_backend(api: &WorkspaceApi) -> Result<SqliteTicketBackend> {
let config = ticket::config::TicketConfig::load_workspace(&api.config.workspace_root)
.map_err(|error| Error::Config(format!("load Ticket workspace settings: {error}")))?;
Ok(SqliteTicketBackend::new(
api.config.database_path.clone(),
api.config.workspace_id.clone(),
)
.with_record_language(config.ticket_record_language()))
}
fn browser_ticket_detail(api: &WorkspaceApi, ticket_id: &str) -> ApiResult<Json<TicketDetail>> {
Ok(Json(api.authority.ticket(ticket_id)?))
}
async fn scoped_edit_ticket_item(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
Json(request): Json<BrowserEditTicketRequest>,
) -> ApiResult<Json<TicketDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
if let Some(TicketTargetEdit::Set { repository_id, .. }) = request.target.as_ref() {
if !api
.store
.list_repositories(&api.config.workspace_id)?
.iter()
.any(|repository| repository.repository_id == *repository_id)
{
return Err(settings_bad_request(
"unknown_ticket_repository",
"repository_id must identify a repository registered in this Workspace",
));
}
}
browser_ticket_backend(&api)?
.edit_item(
TicketIdOrSlug::Id(path.id.clone()),
TicketItemEdit {
title: request.title,
body: request.body.map(MarkdownText::new),
body_replacement: match (request.old_string, request.new_string) {
(Some(old_string), Some(new_string)) => Some(TicketBodyReplacement {
old_string,
new_string,
replace_all: request.replace_all,
}),
(None, None) => None,
_ => {
return Err(settings_bad_request(
"invalid_ticket_edit_replacement",
"old_string and new_string must be provided together",
));
}
},
target: request.target,
author: request.author,
},
)
.map_err(Error::from)?;
browser_ticket_detail(&api, &path.id)
}
async fn scoped_transition_ticket_state(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
Json(request): Json<BrowserTransitionTicketStateRequest>,
) -> ApiResult<Json<TicketDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let current = api.authority.ticket(&path.id)?;
let mut change = TicketStateChange::new(
current.state,
request.state.as_str(),
request
.reason
.unwrap_or_else(|| "state changed from Web Ticket API".to_owned()),
request.body.unwrap_or_default(),
);
change.author = request.author;
browser_ticket_backend(&api)?
.set_workflow_state(TicketIdOrSlug::Id(path.id.clone()), change)
.map_err(Error::from)?;
browser_ticket_detail(&api, &path.id)
}
async fn scoped_append_ticket_event(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
Json(request): Json<BrowserAppendTicketEventRequest>,
) -> ApiResult<Json<TicketDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let mut event = NewTicketEvent::new(request.role.into(), request.body);
event.author = request.author;
browser_ticket_backend(&api)?
.add_event(TicketIdOrSlug::Id(path.id.clone()), event)
.map_err(Error::from)?;
browser_ticket_detail(&api, &path.id)
}
async fn scoped_review_ticket(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
Json(request): Json<BrowserReviewTicketRequest>,
) -> ApiResult<Json<TicketDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
browser_ticket_backend(&api)?
.review(
TicketIdOrSlug::Id(path.id.clone()),
TicketReview {
result: request.result,
body: MarkdownText::new(request.body),
author: request.author,
},
)
.map_err(Error::from)?;
browser_ticket_detail(&api, &path.id)
}
async fn scoped_queue_ticket(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
Json(request): Json<BrowserQueueTicketRequest>,
) -> ApiResult<Json<TicketDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let queued_by = request.queued_by.as_deref().unwrap_or("web");
browser_ticket_backend(&api)?
.queue_ready(TicketIdOrSlug::Id(path.id.clone()), queued_by)
.map_err(Error::from)?;
let Json(ticket) = browser_ticket_detail(&api, &path.id)?;
notify_ticket_recipients(
&api,
&path.workspace_id,
&path.id,
ticket
.events
.last()
.map(|event| event.sequence as i64)
.unwrap_or_default(),
ticket
.events
.last()
.map(|event| event.kind.as_str())
.unwrap_or("state_changed"),
"queue_ready",
TicketWorkflowState::Ready.as_str(),
ticket.state.as_str(),
None,
);
Ok(Json(ticket))
}
async fn scoped_close_ticket(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRecordPath>,
Json(request): Json<BrowserCloseTicketRequest>,
) -> ApiResult<Json<TicketDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
browser_ticket_backend(&api)?
.close(
TicketIdOrSlug::Id(path.id.clone()),
MarkdownText::new(request.resolution),
)
.map_err(Error::from)?;
browser_ticket_detail(&api, &path.id)
}
async fn execute_worker_ticket_rest_operation(
api: &WorkspaceApi,
workspace_id: &str,
headers: HeaderMap,
mut operation: TicketBackendOperation,
) -> ApiResult<TicketBackendOperationResult> {
validate_workspace_scope(api, workspace_id)?;
let config = ticket::config::TicketConfig::load_workspace(&api.config.workspace_root)
.map_err(|error| Error::Config(format!("load Ticket workspace settings: {error}")))?;
let mut backend = SqliteTicketBackend::new(
api.config.database_path.clone(),
api.config.workspace_id.clone(),
)
.with_record_language(config.ticket_record_language());
let operation_kind = ticket_mutation_operation_kind(&operation);
let is_mutation = operation_kind != "read";
let target = ticket_mutation_target(&operation).cloned();
let source = authenticate_worker_mutation_source(api, workspace_id, &headers)?;
let before = target.as_ref().and_then(|id| backend.show(id.clone()).ok());
let previous_state = before
.as_ref()
.map(|ticket| ticket.meta.workflow_state.as_str().to_string())
.unwrap_or_else(|| ticket_operation_initial_state(&operation));
bind_worker_ticket_operation_source(&source, &mut operation);
let source_context = worker_ticket_source_context(api, workspace_id, &source, before.as_ref());
backend = backend.with_event_attributes(source_context.attributes(operation_kind));
let result = execute_ticket_backend_operation(&backend, operation).map_err(Error::from)?;
if is_mutation
&& let Some(target) = target
&& let Ok(ticket) = backend.show(target)
{
let event = ticket.events.last();
notify_ticket_recipients(
api,
workspace_id,
&ticket.meta.id,
event
.and_then(|event| event.attributes.get("event_sequence"))
.and_then(|value| value.parse::<i64>().ok())
.unwrap_or(ticket.events.len() as i64),
event.map(|event| event.kind.as_str()).unwrap_or("mutation"),
operation_kind,
&previous_state,
ticket.meta.workflow_state.as_str(),
Some((source.runtime_id, source.worker_id)),
);
}
Ok(result)
}
#[cfg(test)]
async fn execute_worker_ticket_test_operation(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
headers: HeaderMap,
Json(operation): Json<TicketBackendOperation>,
) -> ApiResult<Json<TicketBackendOperationResult>> {
execute_worker_ticket_rest_operation(&api, &path.workspace_id, headers, operation)
.await
.map(Json)
}
fn ticket_rest_result<T>(
result: TicketBackendOperationResult,
extract: impl FnOnce(TicketBackendOperationResult) -> Option<T>,
) -> ApiResult<Json<T>> {
extract(result).map(Json).ok_or_else(|| {
Error::Config("Ticket REST handler received an unexpected backend result".to_string())
.into()
})
}
fn ticket_rest_unit(result: TicketBackendOperationResult) -> ApiResult<StatusCode> {
match result {
TicketBackendOperationResult::Unit => Ok(StatusCode::NO_CONTENT),
_ => Err(Error::Config(
"Ticket REST handler received an unexpected backend result".to_string(),
)
.into()),
}
}
#[derive(Debug, Deserialize)]
struct DefaultIntakeReadyBodyRequest {
from: String,
}
async fn scoped_default_intake_ready_body(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
headers: HeaderMap,
Json(request): Json<DefaultIntakeReadyBodyRequest>,
) -> ApiResult<Json<String>> {
let result = execute_worker_ticket_rest_operation(
&api,
&path.workspace_id,
headers,
TicketBackendOperation::DefaultIntakeReadyStateChangeBody { from: request.from },
)
.await?;
ticket_rest_result(result, |result| match result {
TicketBackendOperationResult::Text(body) => Some(body),
_ => None,
})
}
#[derive(Debug, Default, Deserialize)]
struct TicketSummarySearchQuery {
state: Option<String>,
}
async fn scoped_list_ticket_summaries(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
headers: HeaderMap,
Query(query): Query<TicketSummarySearchQuery>,
) -> ApiResult<Json<Vec<ticket::TicketSummary>>> {
let filter = match query.state.as_deref().unwrap_or("active") {
"active" => ticket::TicketListQuery::active(),
"all" => ticket::TicketListQuery::all(),
states => {
let mut selected = Vec::new();
for state in states.split(',') {
selected.push(ticket::TicketListState::parse(state).ok_or_else(|| {
Error::Ticket(ticket::TicketError::InvalidPathComponent(state.to_string()))
})?);
}
ticket::TicketListQuery::states(selected)
}
};
let result = execute_worker_ticket_rest_operation(
&api,
&path.workspace_id,
headers,
TicketBackendOperation::List { filter },
)
.await?;
ticket_rest_result(result, |result| match result {
TicketBackendOperationResult::Tickets(tickets) => Some(tickets),
_ => None,
})
}
async fn scoped_get_ticket_record(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
) -> ApiResult<Json<ticket::Ticket>> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::Show {
id: TicketIdOrSlug::Query(id),
},
)
.await?;
ticket_rest_result(result, |result| match result {
TicketBackendOperationResult::Ticket(ticket) => Some(ticket),
_ => None,
})
}
async fn scoped_create_ticket_record(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
headers: HeaderMap,
Json(input): Json<ticket::NewTicket>,
) -> ApiResult<Json<ticket::TicketRef>> {
let result = execute_worker_ticket_rest_operation(
&api,
&path.workspace_id,
headers,
TicketBackendOperation::Create { input },
)
.await?;
ticket_rest_result(result, |result| match result {
TicketBackendOperationResult::TicketRef(ticket) => Some(ticket),
_ => None,
})
}
async fn scoped_edit_ticket_record_item(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
Json(edit): Json<TicketItemEdit>,
) -> ApiResult<Json<ticket::Ticket>> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::EditItem {
id: TicketIdOrSlug::Query(id),
edit,
},
)
.await?;
ticket_rest_result(result, |result| match result {
TicketBackendOperationResult::Ticket(ticket) => Some(ticket),
_ => None,
})
}
async fn scoped_ticket_dependency_check(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
) -> ApiResult<Json<ticket::TicketDependencyCheck>> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::DependencyCheck {
id: TicketIdOrSlug::Query(id),
},
)
.await?;
ticket_rest_result(result, |result| match result {
TicketBackendOperationResult::DependencyCheck(check) => Some(check),
_ => None,
})
}
async fn scoped_add_ticket_thread_event(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
Json(event): Json<NewTicketEvent>,
) -> ApiResult<StatusCode> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::AddEvent {
id: TicketIdOrSlug::Query(id),
event,
},
)
.await?;
ticket_rest_unit(result)
}
async fn scoped_add_ticket_state_change(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
Json(change): Json<TicketStateChange>,
) -> ApiResult<StatusCode> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::AddStateChanged {
id: TicketIdOrSlug::Query(id),
change,
},
)
.await?;
ticket_rest_unit(result)
}
async fn scoped_add_ticket_intake_summary(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
Json(summary): Json<ticket::TicketIntakeSummary>,
) -> ApiResult<StatusCode> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::AddIntakeSummary {
id: TicketIdOrSlug::Query(id),
summary,
},
)
.await?;
ticket_rest_unit(result)
}
#[derive(Debug, Deserialize)]
struct TicketIntakeReadyRequest {
summary: ticket::TicketIntakeSummary,
change: TicketStateChange,
}
async fn scoped_set_ticket_state_field(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id, field)): AxumPath<(String, String, String)>,
headers: HeaderMap,
Json(change): Json<TicketStateChange>,
) -> ApiResult<StatusCode> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::SetStateField {
id: TicketIdOrSlug::Query(id),
field,
change,
},
)
.await?;
ticket_rest_unit(result)
}
async fn scoped_set_ticket_workflow_state(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
Json(change): Json<TicketStateChange>,
) -> ApiResult<StatusCode> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::SetWorkflowState {
id: TicketIdOrSlug::Query(id),
change,
},
)
.await?;
ticket_rest_unit(result)
}
async fn scoped_prepare_ticket_intake_ready(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
Json(request): Json<TicketIntakeReadyRequest>,
) -> ApiResult<StatusCode> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::MarkIntakeReady {
id: TicketIdOrSlug::Query(id),
summary: request.summary,
change: request.change,
},
)
.await?;
ticket_rest_unit(result)
}
async fn scoped_queue_ticket_record(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
) -> ApiResult<StatusCode> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::QueueReady {
id: TicketIdOrSlug::Query(id),
queued_by: String::new(),
},
)
.await?;
ticket_rest_unit(result)
}
async fn scoped_review_ticket_record(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
Json(review): Json<TicketReview>,
) -> ApiResult<StatusCode> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::Review {
id: TicketIdOrSlug::Query(id),
review,
},
)
.await?;
ticket_rest_unit(result)
}
async fn scoped_close_ticket_record(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
Json(resolution): Json<MarkdownText>,
) -> ApiResult<StatusCode> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::Close {
id: TicketIdOrSlug::Query(id),
resolution,
},
)
.await?;
ticket_rest_unit(result)
}
async fn scoped_record_ticket_relation(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
Json(relation): Json<ticket::NewTicketRelation>,
) -> ApiResult<Json<ticket::TicketRelation>> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::AddTicketRelation {
id: TicketIdOrSlug::Query(id),
relation,
},
)
.await?;
ticket_rest_result(result, |result| match result {
TicketBackendOperationResult::Relation(relation) => Some(relation),
_ => None,
})
}
#[derive(Debug, Deserialize)]
struct TicketRelationSearchRequest {
ticket: Option<TicketIdOrSlug>,
kind: Option<ticket::TicketRelationKind>,
}
async fn scoped_query_ticket_relations(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
headers: HeaderMap,
Json(query): Json<TicketRelationSearchRequest>,
) -> ApiResult<Json<Vec<ticket::TicketRelation>>> {
let result = execute_worker_ticket_rest_operation(
&api,
&path.workspace_id,
headers,
TicketBackendOperation::QueryTicketRelations {
ticket: query.ticket,
kind: query.kind,
},
)
.await?;
ticket_rest_result(result, |result| match result {
TicketBackendOperationResult::Relations(relations) => Some(relations),
_ => None,
})
}
async fn scoped_ticket_relation_view(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
) -> ApiResult<Json<ticket::TicketRelationView>> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::RelationView {
id: TicketIdOrSlug::Query(id),
},
)
.await?;
ticket_rest_result(result, |result| match result {
TicketBackendOperationResult::RelationView(view) => Some(view),
_ => None,
})
}
async fn scoped_record_ticket_orchestration_plan(
State(api): State<WorkspaceApi>,
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
headers: HeaderMap,
Json(record): Json<ticket::NewOrchestrationPlanRecord>,
) -> ApiResult<Json<ticket::OrchestrationPlanRecord>> {
let result = execute_worker_ticket_rest_operation(
&api,
&workspace_id,
headers,
TicketBackendOperation::AddOrchestrationPlanRecord {
id: TicketIdOrSlug::Query(id),
record,
},
)
.await?;
ticket_rest_result(result, |result| match result {
TicketBackendOperationResult::OrchestrationPlanRecord(record) => Some(record),
_ => None,
})
}
#[derive(Debug, Deserialize)]
struct TicketOrchestrationPlanSearchRequest {
ticket: Option<TicketIdOrSlug>,
kind: Option<ticket::OrchestrationPlanKind>,
}
async fn scoped_query_ticket_orchestration_plans(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
headers: HeaderMap,
Json(query): Json<TicketOrchestrationPlanSearchRequest>,
) -> ApiResult<Json<Vec<ticket::OrchestrationPlanRecord>>> {
let result = execute_worker_ticket_rest_operation(
&api,
&path.workspace_id,
headers,
TicketBackendOperation::QueryOrchestrationPlanRecords {
ticket: query.ticket,
kind: query.kind,
},
)
.await?;
ticket_rest_result(result, |result| match result {
TicketBackendOperationResult::OrchestrationPlanRecords(records) => Some(records),
_ => None,
})
}
async fn scoped_ticket_doctor(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
headers: HeaderMap,
) -> ApiResult<Json<ticket::TicketDoctorReport>> {
let result = execute_worker_ticket_rest_operation(
&api,
&path.workspace_id,
headers,
TicketBackendOperation::Doctor,
)
.await?;
ticket_rest_result(result, |result| match result {
TicketBackendOperationResult::DoctorReport(report) => Some(report),
_ => None,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct WorkerMutationSource {
runtime_id: String,
worker_id: String,
}
fn ticket_mutation_target(operation: &TicketBackendOperation) -> Option<&TicketIdOrSlug> {
match operation {
TicketBackendOperation::EditItem { id, .. }
| TicketBackendOperation::AddEvent { id, .. }
| TicketBackendOperation::AddStateChanged { id, .. }
| TicketBackendOperation::AddIntakeSummary { id, .. }
| TicketBackendOperation::SetStateField { id, .. }
| TicketBackendOperation::SetWorkflowState { id, .. }
| TicketBackendOperation::MarkIntakeReady { id, .. }
| TicketBackendOperation::QueueReady { id, .. }
| TicketBackendOperation::Review { id, .. }
| TicketBackendOperation::Close { id, .. }
| TicketBackendOperation::AddTicketRelation { id, .. }
| TicketBackendOperation::AddOrchestrationPlanRecord { id, .. } => Some(id),
_ => None,
}
}
fn bind_worker_ticket_operation_source(
source: &WorkerMutationSource,
operation: &mut TicketBackendOperation,
) {
let author = format!("worker:{}/{}", source.runtime_id, source.worker_id);
match operation {
TicketBackendOperation::Create { input } => input.author = Some(author),
TicketBackendOperation::EditItem { edit, .. } => edit.author = Some(author),
TicketBackendOperation::AddEvent { event, .. } => event.author = Some(author),
TicketBackendOperation::AddStateChanged { change, .. }
| TicketBackendOperation::SetStateField { change, .. }
| TicketBackendOperation::SetWorkflowState { change, .. } => change.author = Some(author),
TicketBackendOperation::AddIntakeSummary { summary, .. } => summary.author = Some(author),
TicketBackendOperation::MarkIntakeReady {
summary, change, ..
} => {
summary.author = Some(author.clone());
change.author = Some(author);
}
TicketBackendOperation::QueueReady { queued_by, .. } => *queued_by = author,
TicketBackendOperation::Review { review, .. } => review.author = Some(author),
TicketBackendOperation::AddTicketRelation { relation, .. } => {
relation.author = Some(author)
}
TicketBackendOperation::AddOrchestrationPlanRecord { record, .. } => {
record.author = Some(author)
}
_ => {}
}
}
fn ticket_mutation_operation_kind(operation: &TicketBackendOperation) -> &'static str {
match operation {
TicketBackendOperation::Create { .. } => "create",
TicketBackendOperation::EditItem { .. } => "edit_item",
TicketBackendOperation::AddEvent { .. } => "add_event",
TicketBackendOperation::AddStateChanged { .. } => "add_state_changed",
TicketBackendOperation::AddIntakeSummary { .. } => "add_intake_summary",
TicketBackendOperation::SetStateField { .. } => "set_state_field",
TicketBackendOperation::SetWorkflowState { .. } => "set_workflow_state",
TicketBackendOperation::MarkIntakeReady { .. } => "mark_intake_ready",
TicketBackendOperation::QueueReady { .. } => "queue_ready",
TicketBackendOperation::Review { .. } => "review",
TicketBackendOperation::Close { .. } => "close",
TicketBackendOperation::AddTicketRelation { .. } => "add_relation",
TicketBackendOperation::AddOrchestrationPlanRecord { .. } => "add_plan_record",
_ => "read",
}
}
fn ticket_operation_initial_state(operation: &TicketBackendOperation) -> String {
match operation {
TicketBackendOperation::Create { input } => input
.workflow_state
.as_ref()
.map(|state| state.as_str().to_string())
.unwrap_or_else(|| TicketWorkflowState::Planning.as_str().to_string()),
_ => TicketWorkflowState::Planning.as_str().to_string(),
}
}
#[derive(Debug, Clone)]
struct WorkerTicketSourceContext {
runtime_id: String,
worker_id: String,
actor_role: String,
assignment_id: Option<String>,
}
impl WorkerTicketSourceContext {
fn attributes(&self, operation_kind: &str) -> BTreeMap<String, String> {
let mut attributes = BTreeMap::from([
("source_runtime_id".to_string(), self.runtime_id.clone()),
("source_worker_id".to_string(), self.worker_id.clone()),
("source_actor_role".to_string(), self.actor_role.clone()),
(
"source_operation_kind".to_string(),
operation_kind.to_string(),
),
]);
if let Some(assignment_id) = &self.assignment_id {
attributes.insert("source_assignment_id".to_string(), assignment_id.clone());
}
attributes
}
}
fn worker_source_actor_role(is_current_assignment: bool, is_orchestrator: bool) -> &'static str {
if is_current_assignment {
"coder"
} else if is_orchestrator {
"orchestrator"
} else {
"worker"
}
}
fn worker_ticket_source_context(
api: &WorkspaceApi,
workspace_id: &str,
source: &WorkerMutationSource,
ticket: Option<&ticket::Ticket>,
) -> WorkerTicketSourceContext {
let assignment = ticket.and_then(|ticket| {
api.store
.get_current_ticket_worker_assignment(workspace_id, &ticket.meta.id)
.ok()
.flatten()
});
let orchestrator = find_workspace_orchestrator(api);
let is_current_assignment = assignment.as_ref().is_some_and(|assignment| {
assignment.runtime_id == source.runtime_id && assignment.worker_id == source.worker_id
});
let is_orchestrator = orchestrator.as_ref().is_some_and(|worker| {
worker.runtime_id == source.runtime_id && worker.worker_id == source.worker_id
});
let actor_role = worker_source_actor_role(is_current_assignment, is_orchestrator);
WorkerTicketSourceContext {
runtime_id: source.runtime_id.clone(),
worker_id: source.worker_id.clone(),
actor_role: actor_role.to_string(),
assignment_id: assignment.and_then(|assignment| {
(assignment.runtime_id == source.runtime_id && assignment.worker_id == source.worker_id)
.then_some(assignment.assignment_id)
}),
}
}
fn notify_ticket_recipients(
api: &WorkspaceApi,
workspace_id: &str,
ticket_id: &str,
event_sequence: i64,
event_kind: &str,
source_operation_kind: &str,
previous_state: &str,
current_state: &str,
source: Option<(String, String)>,
) {
let mut recipients = Vec::new();
if let Some(assignment) = api
.store
.get_current_ticket_worker_assignment(workspace_id, ticket_id)
.ok()
.flatten()
{
recipients.push((assignment.runtime_id, assignment.worker_id));
}
if (matches!(previous_state, "queued" | "inprogress")
|| matches!(current_state, "queued" | "inprogress"))
&& let Some(orchestrator) = find_workspace_orchestrator(api)
{
recipients.push((orchestrator.runtime_id, orchestrator.worker_id));
}
recipients.sort();
recipients.dedup();
let source_fields = source
.as_ref()
.map(|(runtime_id, worker_id)| {
format!(" source_runtime_id={runtime_id} source_worker_id={worker_id}")
})
.unwrap_or_default();
for (runtime_id, worker_id) in recipients {
if source
.as_ref()
.is_some_and(|source| source.0 == runtime_id && source.1 == worker_id)
{
continue;
}
let _ = api.runtime.send_input(
&runtime_id,
&worker_id,
WorkerInputRequest {
kind: WorkerInputKind::Notify,
content: format!(
"Ticket notification: workspace_id={workspace_id} ticket_id={ticket_id} event_sequence={event_sequence} event_kind={event_kind} source_operation_kind={source_operation_kind}.{source_fields} Reread the Ticket before acting.",
),
segments: None,
},
);
}
}
fn authenticate_worker_mutation_source(
api: &WorkspaceApi,
_workspace_id: &str,
headers: &HeaderMap,
) -> Result<WorkerMutationSource> {
let runtime_id = headers
.get("x-yoi-runtime-id")
.and_then(|value| value.to_str().ok())
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| Error::WorkerSourceIdentity("missing Runtime id".to_string()))?;
let worker_id = headers
.get("x-yoi-worker-id")
.and_then(|value| value.to_str().ok())
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| {
Error::WorkerSourceIdentity("missing Runtime-bound Worker id".to_string())
})?;
api.runtime.worker(runtime_id, worker_id).map_err(|_| {
Error::WorkerSourceIdentity("Runtime-bound Worker identity does not exist".to_string())
})?;
Ok(WorkerMutationSource {
runtime_id: runtime_id.to_string(),
worker_id: worker_id.to_string(),
})
}
async fn run_orchestrator_turn_end_hook(api: WorkspaceApi) {
let Ok(mut subscription) = api.runtime_subscription_broker.subscribe(
EMBEDDED_WORKER_RUNTIME_ID,
protocol::subscription::EventSubscriptionSelector::RuntimeWorkers,
) else {
return;
};
let mut worker_states = HashMap::new();
while let Some(update) = subscription.recv().await {
match update {
crate::runtime_subscription::BrokerSubscriptionEvent::Snapshot { snapshot, .. } => {
if let protocol::subscription::SubscriptionSnapshot::Workers { workers } = snapshot
{
worker_states.clear();
for worker in workers {
let worker_id = worker.worker_id.to_string();
maybe_dispatch_orchestrator_turn_end(&api, &worker_id, None, worker.state);
worker_states.insert(worker_id, worker.state);
}
}
}
crate::runtime_subscription::BrokerSubscriptionEvent::Event { payload, .. } => {
match payload {
protocol::subscription::SubscriptionEventPayload::WorkerUpserted { worker } => {
let worker_id = worker.worker_id.to_string();
let previous = worker_states.insert(worker_id.clone(), worker.state);
maybe_dispatch_orchestrator_turn_end(
&api,
&worker_id,
previous,
worker.state,
);
}
protocol::subscription::SubscriptionEventPayload::WorkerRemoved {
worker_id,
..
} => {
worker_states.remove(worker_id.as_str());
}
_ => {}
}
}
crate::runtime_subscription::BrokerSubscriptionEvent::Disconnected { .. } => {
worker_states.clear();
}
crate::runtime_subscription::BrokerSubscriptionEvent::Rejected { .. }
| crate::runtime_subscription::BrokerSubscriptionEvent::Closed { .. } => return,
}
}
}
fn maybe_dispatch_orchestrator_turn_end(
api: &WorkspaceApi,
worker_id: &str,
previous: Option<protocol::subscription::SubscriptionWorkerState>,
current: protocol::subscription::SubscriptionWorkerState,
) {
use protocol::subscription::SubscriptionWorkerState;
if current != SubscriptionWorkerState::Idle
|| !matches!(
previous,
None | Some(SubscriptionWorkerState::Running)
| Some(SubscriptionWorkerState::Stopped)
| Some(SubscriptionWorkerState::Paused)
)
{
return;
}
let Some(orchestrator) = find_workspace_orchestrator(api) else {
return;
};
if orchestrator.runtime_id == EMBEDDED_WORKER_RUNTIME_ID && orchestrator.worker_id == worker_id
{
dispatch_orchestrator_queue_attention(api);
}
}
fn dispatch_orchestrator_queue_attention(api: &WorkspaceApi) {
let Some(orchestrator) = find_workspace_orchestrator(api) else {
return;
};
let Ok(backend) = browser_ticket_backend(api) else {
return;
};
let Ok(mut queued) = backend.list(ticket::TicketListQuery::states([
ticket::TicketListState::Queued,
])) else {
return;
};
queued.sort_by(|left, right| left.id.cmp(&right.id));
if queued.is_empty() {
*api.orchestrator_attention_fingerprint
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
return;
}
let Ok(inprogress) = backend.list(ticket::TicketListQuery::states([
ticket::TicketListState::InProgress,
])) else {
return;
};
if !inprogress.is_empty() {
return;
}
let fingerprint = queued
.iter()
.map(|ticket| ticket.id.as_str())
.collect::<Vec<_>>()
.join("|");
if api
.orchestrator_attention_fingerprint
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.as_deref()
== Some(fingerprint.as_str())
{
return;
}
let shown = queued
.iter()
.take(ORCHESTRATOR_ATTENTION_TICKET_LIMIT)
.map(|ticket| {
format!(
"- {}{}",
bounded_orchestrator_attention_text(&ticket.id, 80),
bounded_orchestrator_attention_text(&ticket.title, 240)
)
})
.collect::<Vec<_>>()
.join("\n");
let omitted = queued
.len()
.saturating_sub(ORCHESTRATOR_ATTENTION_TICKET_LIMIT);
let omitted_line = if omitted == 0 {
String::new()
} else {
format!("Additional queued Tickets omitted from this notice: {omitted}\n")
};
let content = ORCHESTRATOR_ATTENTION_PROMPT
.replace("{{omitted_line}}", &omitted_line)
.replace("{{workspace_id}}", &api.config.workspace_id)
.replace("{{ticket_lines}}", &shown);
let accepted = api
.runtime
.send_input(
&orchestrator.runtime_id,
&orchestrator.worker_id,
WorkerInputRequest {
kind: WorkerInputKind::Notify,
content,
segments: None,
},
)
.is_ok_and(|result| result.state == WorkerOperationState::Accepted);
if accepted {
*api.orchestrator_attention_fingerprint
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(fingerprint);
}
}
fn bounded_orchestrator_attention_text(input: &str, max_chars: usize) -> String {
let mut output = String::new();
for (index, character) in input.chars().enumerate() {
if index == max_chars {
output.push('…');
break;
}
output.push(if character.is_control() {
' '
} else {
character
});
}
output
}
fn find_workspace_orchestrator(api: &WorkspaceApi) -> Option<WorkerSummary> {
let is_orchestrator = |worker: &WorkerSummary| {
worker.singleton_key.as_deref() == Some(crate::hosts::WORKSPACE_ORCHESTRATOR_SINGLETON_KEY)
};
if let Some(worker) = api
.runtime
.list_workers(1000)
.items
.into_iter()
.find(is_orchestrator)
{
return Some(worker);
}
for runtime in api.runtime.list_runtimes(1000).items {
if let Ok(stopped) = api
.runtime
.list_stopped_workers_for_runtime(&runtime.runtime_id, 1000)
{
if let Some(worker) = stopped.items.into_iter().find(is_orchestrator) {
return Some(worker);
}
}
}
None
}
#[derive(Debug, Clone, Serialize)]
struct MemoryDocumentResponse {
body_md: String,
created_at: String,
updated_at: String,
bytes: usize,
record_source: String,
}
async fn scoped_get_memory_document(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<MemoryDocumentResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let document = api.authority.memory_document()?;
Ok(Json(MemoryDocumentResponse {
bytes: document.body_md.len(),
body_md: document.body_md,
created_at: document.created_at,
updated_at: document.updated_at,
record_source: document.record_source,
}))
}
async fn scoped_list_memory_staging(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Query(query): Query<MemoryStagingQuery>,
) -> ApiResult<Json<MemoryStagingListResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(list_memory_staging_from_authority(
&api.authority,
query.limit,
)?))
}
async fn scoped_memory_backend_operation(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(operation): Json<MemoryBackendOperation>,
) -> ApiResult<Json<MemoryBackendHttpResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let response = match execute_memory_backend_operation_with_authority(&api.authority, operation)
{
Ok(result) => MemoryBackendHttpResponse::Ok { result },
Err(error) => MemoryBackendHttpResponse::Error {
message: sanitize_backend_error(&error.to_string()),
},
};
Ok(Json(response))
}
const MEMORY_CONSOLIDATION_PROFILE: &str = "memory-consolidation";
const MEMORY_CONSOLIDATION_SINGLETON_KEY: &str = "workspace-memory-consolidation";
const MEMORY_CONSOLIDATION_WORKER_SCAN_LIMIT: usize = 100;
async fn scoped_memory_consolidation(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(operation): Json<MemoryConsolidateStagingOperation>,
) -> ApiResult<Json<MemoryConsolidationOutput>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(start_memory_staging_consolidation(api, operation)?))
}
fn start_memory_staging_consolidation(
api: WorkspaceApi,
operation: MemoryConsolidateStagingOperation,
) -> ApiResult<MemoryConsolidationOutput> {
let backlog = memory_staging_backlog_from_authority(&api.authority)?;
let candidate_count = backlog.candidate_count;
let total_bytes = backlog.total_bytes;
if candidate_count == 0 {
return Ok(MemoryConsolidationOutput {
status: "skipped_empty".to_string(),
summary: "No Memory staging candidates are pending.".to_string(),
candidate_count,
total_bytes,
});
}
let reached_files = operation
.threshold_files
.is_some_and(|threshold| candidate_count >= threshold);
let reached_bytes = operation
.threshold_bytes
.is_some_and(|threshold| total_bytes >= threshold);
if !operation.force && !reached_files && !reached_bytes {
return Ok(MemoryConsolidationOutput {
status: "skipped_below_threshold".to_string(),
summary: format!(
"Memory staging backlog has {candidate_count} candidate(s), {total_bytes} byte(s), below configured threshold."
),
candidate_count,
total_bytes,
});
}
let runtime_id = select_memory_consolidation_runtime(&api)?;
let input_content = memory_consolidation_input_content(candidate_count, total_bytes);
if let Some(output) = try_reuse_memory_consolidation_worker(
&api,
&runtime_id,
&input_content,
candidate_count,
total_bytes,
)? {
return Ok(output);
}
let profile_selector = ProfileSelector::Builtin(MEMORY_CONSOLIDATION_PROFILE.to_string());
let resolved_config_bundle = crate::profile_settings::build_workspace_profile_config_bundle(
&api.config.workspace_root,
&api.config.workspace_id,
&api.config.workspace_created_at,
MEMORY_CONSOLIDATION_PROFILE,
)?;
let input = EmbeddedWorkerInput {
kind: EmbeddedWorkerInputKind::User,
content: input_content,
segments: None,
};
let result = api.spawn_workspace_worker(
&runtime_id,
WorkerSpawnRequest {
requested_worker_name: Some(MEMORY_CONSOLIDATION_PROFILE.to_string()),
intent: WorkerSpawnIntent::WorkspaceOrchestrator,
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 1,
},
profile: profile_selector,
ticket_assignment: None,
initial_input: Some(input),
working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None,
resolved_config_bundle,
resolved_workspace_api: None,
},
)?;
if result.state != WorkerOperationState::Accepted {
return Ok(MemoryConsolidationOutput {
status: "skipped_spawn_rejected".to_string(),
summary: "Runtime rejected Memory consolidater spawn.".to_string(),
candidate_count,
total_bytes,
});
}
let Some(worker) = result.worker else {
return Ok(MemoryConsolidationOutput {
status: "skipped_spawn_missing_worker".to_string(),
summary:
"Runtime accepted Memory consolidater spawn without returning a Worker summary."
.to_string(),
candidate_count,
total_bytes,
});
};
Ok(MemoryConsolidationOutput {
status: "started".to_string(),
summary: format!(
"Started Memory consolidater '{}' for {candidate_count} staging candidate(s).",
worker.worker_id
),
candidate_count,
total_bytes,
})
}
fn try_reuse_memory_consolidation_worker(
api: &WorkspaceApi,
runtime_id: &str,
input_content: &str,
candidate_count: usize,
total_bytes: u64,
) -> ApiResult<Option<MemoryConsolidationOutput>> {
let workers = api
.runtime
.list_workers_for_runtime(runtime_id, MEMORY_CONSOLIDATION_WORKER_SCAN_LIMIT)
.map_err(|err| err.into_error())?;
let mut consolidaters = workers
.items
.into_iter()
.filter(is_memory_consolidation_worker)
.collect::<Vec<_>>();
if consolidaters.is_empty() {
return Ok(None);
}
consolidaters.sort_by(|a, b| a.worker_id.cmp(&b.worker_id));
if let Some(worker) = consolidaters.iter().find(|worker| worker.state != "idle") {
return Ok(Some(MemoryConsolidationOutput {
status: "skipped_existing_not_idle".to_string(),
summary: format!(
"Existing Memory consolidater '{}' is '{}', not confirmed idle.",
worker.worker_id, worker.state
),
candidate_count,
total_bytes,
}));
}
let worker = consolidaters
.first()
.expect("non-empty consolidater list")
.clone();
let input = api
.runtime
.send_input(
&worker.runtime_id,
&worker.worker_id,
WorkerInputRequest {
kind: WorkerInputKind::User,
content: input_content.to_string(),
segments: None,
},
)
.map_err(|err| err.into_error())?;
if input.state != WorkerOperationState::Accepted {
return Ok(Some(MemoryConsolidationOutput {
status: "skipped_existing_input_rejected".to_string(),
summary: format!(
"Existing idle Memory consolidater '{}' rejected the new consolidation input.",
worker.worker_id
),
candidate_count,
total_bytes,
}));
}
Ok(Some(MemoryConsolidationOutput {
status: "reused".to_string(),
summary: format!(
"Reused Memory consolidater '{}' for {candidate_count} staging candidate(s).",
worker.worker_id
),
candidate_count,
total_bytes,
}))
}
fn is_memory_consolidation_worker(worker: &WorkerSummary) -> bool {
worker.singleton_key.as_deref() == Some(MEMORY_CONSOLIDATION_SINGLETON_KEY)
|| worker.profile.as_deref() == Some(MEMORY_CONSOLIDATION_PROFILE)
}
fn memory_consolidation_input_content(candidate_count: usize, total_bytes: u64) -> String {
format!(
"Process pending Memory staging candidates through MemoryStagingList, MemoryStagingRead, Memory tools, and MemoryStagingClose. Current backlog: {candidate_count} candidate(s), {total_bytes} byte(s)."
)
}
fn select_memory_consolidation_runtime(api: &WorkspaceApi) -> ApiResult<String> {
let runtimes = api.runtime.list_runtimes(100);
if let Some(runtime) = runtimes
.items
.iter()
.find(|runtime| {
runtime.runtime_id == EMBEDDED_WORKER_RUNTIME_ID
&& runtime.capabilities.can_spawn_worker
})
.or_else(|| {
runtimes
.items
.iter()
.find(|runtime| runtime.capabilities.can_spawn_worker)
})
{
return Ok(runtime.runtime_id.clone());
}
Err(Error::RuntimeOperationFailed {
runtime_id: "memory-consolidation".to_string(),
code: "memory_consolidation_runtime_unavailable".to_string(),
message: "No runtime capable of spawning the Memory consolidater is available".to_string(),
}
.into())
}
async fn scoped_list_skills(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<worker::skill::SkillCatalogResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(skills::catalog(&api.config.workspace_root)))
}
async fn scoped_lint_skills(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<worker::skill::SkillCatalogResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(skills::lint(&api.config.workspace_root)))
}
async fn scoped_get_skill(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedSkillPath>,
) -> ApiResult<Json<worker::skill::SkillDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
skills::detail(&api.config.workspace_root, &path.name)
.map(Json)
.map_err(skill_api_error)
}
async fn scoped_activate_skill(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedSkillPath>,
) -> ApiResult<Json<worker::skill::SkillActivationResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
skills::activation(&api.config.workspace_root, &path.name)
.map(Json)
.map_err(skill_api_error)
}
fn skill_api_error(error: skills::SkillError) -> ApiError {
match error {
skills::SkillError::NotFound(name) => ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: "workspace".to_string(),
code: "skill_not_found".to_string(),
message: format!("unknown Skill `{name}`"),
},
vec![RuntimeDiagnostic {
code: "skill_not_found".to_string(),
severity: DiagnosticSeverity::Error,
message: format!("unknown Skill `{name}`"),
}],
),
skills::SkillError::Io(error) => ApiError::from(Error::Io(error)),
}
}
async fn scoped_list_objectives(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Query(query): Query<ObjectiveListQuery>,
) -> ApiResult<Json<ListResponse<crate::records::ObjectiveSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_objectives(State(api), Query(query)).await
}
async fn scoped_get_objective(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedObjectivePath>,
) -> ApiResult<Json<ObjectiveDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_objective(State(api), AxumPath(path.objective_id)).await
}
async fn scoped_create_objective(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(request): Json<ObjectiveCreateRequest>,
) -> ApiResult<Json<ObjectiveDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(api.authority.create_objective(
ObjectiveCreateInput {
title: request.title,
body_md: request.body_md,
state: request.state,
linked_tickets: request.linked_tickets,
},
)?))
}
async fn scoped_edit_objective(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedObjectivePath>,
Json(request): Json<ObjectiveEditRequest>,
) -> ApiResult<Json<ObjectiveDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(api.authority.edit_objective(
&path.objective_id,
ObjectiveEditInput {
title: request.title,
old_string: request.old_string,
new_string: request.new_string,
replace_all: request.replace_all,
},
)?))
}
async fn scoped_set_objective_state(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedObjectivePath>,
Json(request): Json<ObjectiveStateRequest>,
) -> ApiResult<Json<ObjectiveDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(
api.authority
.set_objective_state(&path.objective_id, &request.state)?,
))
}
async fn scoped_link_objective_ticket(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedObjectivePath>,
Json(request): Json<ObjectiveLinkTicketRequest>,
) -> ApiResult<Json<ObjectiveDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(api.authority.link_objective_ticket(
&path.objective_id,
&request.ticket_id,
)?))
}
async fn scoped_unlink_objective_ticket(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedObjectiveTicketPath>,
) -> ApiResult<Json<ObjectiveDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(api.authority.unlink_objective_ticket(
&path.objective_id,
&path.ticket_id,
)?))
}
async fn scoped_list_repositories(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<RepositoryListResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_repositories(State(api)).await
}
async fn scoped_repository_detail(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRepositoryPath>,
) -> ApiResult<Json<RepositoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
repository_detail(State(api), AxumPath(path.repository_id)).await
}
async fn scoped_repository_log(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRepositoryPath>,
Query(query): Query<LogQuery>,
) -> ApiResult<Json<RepositoryLogResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
repository_log(State(api), AxumPath(path.repository_id), Query(query)).await
}
async fn scoped_list_hosts(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<RuntimeListResponse<HostSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_hosts(State(api)).await
}
async fn scoped_get_profile_source_archive(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedProfileArchivePath>,
headers: HeaderMap,
) -> ApiResult<(StatusCode, HeaderMap, Vec<u8>)> {
validate_workspace_scope(&api, &path.workspace_id)?;
let archive = api
.resource_broker
.profile_source_archive(&path.digest)
.ok_or_else(|| Error::Store("profile source archive not found".to_string()))?;
let etag = format!("\"profile-source:{}\"", archive.reference.digest);
if headers
.get(IF_NONE_MATCH)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.split(',').any(|candidate| candidate.trim() == etag))
{
let mut response_headers = HeaderMap::new();
response_headers.insert(ETAG, etag.parse().unwrap());
return Ok((StatusCode::NOT_MODIFIED, response_headers, Vec::new()));
}
let mut response_headers = HeaderMap::new();
response_headers.insert(ETAG, etag.parse().unwrap());
response_headers.insert(CONTENT_TYPE, "application/x-tar".parse().unwrap());
Ok((StatusCode::OK, response_headers, archive.content))
}
async fn scoped_list_runtimes(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<RuntimeListResponse<RuntimeSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_runtimes(State(api)).await
}
async fn scoped_workspace_protocol_ws(
State(api): State<WorkspaceApi>,
AxumPath(workspace_id): AxumPath<String>,
ws: axum::extract::ws::WebSocketUpgrade,
) -> std::result::Result<Response, Response> {
validate_workspace_scope(&api, &workspace_id).map_err(|error| error.into_response())?;
Ok(ws
.on_upgrade(move |socket| {
crate::workspace_subscription::serve_workspace_subscription(api, socket)
})
.into_response())
}
async fn scoped_list_workers(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_workers(State(api)).await
}
async fn scoped_workspace_orchestrator_status(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<BrowserWorkspaceOrchestratorResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Ok(Json(workspace_orchestrator_response(&api, "observed")))
}
async fn scoped_start_workspace_orchestrator(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<BrowserWorkspaceOrchestratorResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let _guard = api.orchestrator_spawn_lock.lock().map_err(|_| {
ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
code: "workspace_orchestrator_spawn_lock_poisoned".to_string(),
message: "Workspace Orchestrator launch lock is unavailable".to_string(),
},
Vec::new(),
)
})?;
if let Some(existing) = find_workspace_orchestrator(&api) {
if workspace_orchestrator_is_online(&existing) {
return Ok(Json(workspace_orchestrator_response(&api, "existing")));
}
let restored = api
.runtime
.restore_worker(&existing.runtime_id, &existing.worker_id)
.map_err(|error| error.into_error())?;
if restored.state != WorkerOperationState::Accepted {
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: existing.runtime_id,
code: "workspace_orchestrator_restore_rejected".to_string(),
message: "Runtime rejected Workspace Orchestrator restore".to_string(),
},
restored.diagnostics,
));
}
*api.orchestrator_attention_fingerprint
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
dispatch_orchestrator_queue_attention(&api);
return Ok(Json(workspace_orchestrator_response(&api, "restored")));
}
let result = api.spawn_workspace_worker(
EMBEDDED_WORKER_RUNTIME_ID,
WorkerSpawnRequest {
requested_worker_name: Some(
crate::hosts::WORKSPACE_ORCHESTRATOR_SINGLETON_KEY.to_string(),
),
intent: WorkerSpawnIntent::WorkspaceOrchestrator,
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 0,
},
profile: ProfileSelector::Builtin("builtin:orchestrator".to_string()),
ticket_assignment: None,
initial_input: None,
working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None,
resolved_config_bundle: None,
resolved_workspace_api: None,
},
)?;
if result.state != WorkerOperationState::Accepted || result.worker.is_none() {
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
code: "workspace_orchestrator_spawn_rejected".to_string(),
message: "Embedded Runtime rejected Workspace Orchestrator launch".to_string(),
},
result.diagnostics,
));
}
*api.orchestrator_attention_fingerprint
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
dispatch_orchestrator_queue_attention(&api);
Ok(Json(workspace_orchestrator_response(&api, "created")))
}
fn workspace_orchestrator_response(
api: &WorkspaceApi,
disposition: &str,
) -> BrowserWorkspaceOrchestratorResponse {
let worker = find_workspace_orchestrator(api);
let online = worker
.as_ref()
.is_some_and(workspace_orchestrator_is_online);
let diagnostics = worker
.as_ref()
.map(|worker| worker.diagnostics.clone())
.unwrap_or_default();
BrowserWorkspaceOrchestratorResponse {
workspace_id: api.config.workspace_id.clone(),
online,
disposition: disposition.to_string(),
worker,
diagnostics,
}
}
fn workspace_orchestrator_is_online(worker: &WorkerSummary) -> bool {
matches!(worker.state.as_str(), "idle" | "running" | "paused")
}
async fn scoped_create_workspace_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(request): Json<BrowserCreateWorkerRequest>,
) -> ApiResult<Json<BrowserCreateWorkerResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
create_workspace_worker(State(api), Json(request)).await
}
async fn scoped_get_worker_launch_options(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<WorkerLaunchOptionsResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_worker_launch_options(State(api)).await
}
async fn scoped_list_runtime_working_directories(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
) -> ApiResult<Json<BrowserWorkingDirectoryListResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let (items, diagnostics) = runtime_working_directory_summaries(&api, &path.runtime_id)?;
Ok(Json(BrowserWorkingDirectoryListResponse {
workspace_id: api.config.workspace_id.clone(),
items,
diagnostics,
}))
}
async fn scoped_create_runtime_working_directory(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Json(mut request): Json<BrowserWorkingDirectoryCreateRequest>,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
request.runtime_id = path.runtime_id;
create_working_directory_for_runtime(api, request)
}
async fn scoped_runtime_working_directory_detail(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkingDirectoryPath>,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
working_directory_detail_for_runtime(api, &path.runtime_id, &path.working_directory_id)
}
async fn scoped_cleanup_runtime_working_directory(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkingDirectoryPath>,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
cleanup_working_directory_for_runtime(api, &path.runtime_id, &path.working_directory_id)
}
async fn scoped_list_working_directories(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<BrowserWorkingDirectoryListResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let items = working_directory_summaries(&api)?;
Ok(Json(BrowserWorkingDirectoryListResponse {
workspace_id: api.config.workspace_id.clone(),
items,
diagnostics: Vec::new(),
}))
}
async fn scoped_create_working_directory(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(_request): Json<BrowserWorkingDirectoryCreateRequest>,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: "workspace-backend".to_string(),
code: "backend_workdir_create_unsupported".to_string(),
message: "Working directory creation must be scoped to a concrete Runtime".to_string(),
},
vec![RuntimeDiagnostic {
code: "backend_workdir_create_unsupported".to_string(),
severity: DiagnosticSeverity::Error,
message: "Use runtime-scoped Worker creation or a runtime-scoped working-directory API; the backend does not own workdir lifecycle.".to_string(),
}],
))
}
async fn scoped_working_directory_detail(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkingDirectoryPath>,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let runtime_id = registered_workdir_runtime_id(&api, &path.working_directory_id)?;
working_directory_detail_for_runtime(api, &runtime_id, &path.working_directory_id)
}
async fn scoped_cleanup_working_directory(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkingDirectoryPath>,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let runtime_id = registered_workdir_runtime_id(&api, &path.working_directory_id)?;
cleanup_working_directory_for_runtime(api, &runtime_id, &path.working_directory_id)
}
fn registered_workdir_runtime_id(
api: &WorkspaceApi,
working_directory_id: &str,
) -> ApiResult<String> {
api.store
.get_workdir_registry(&api.config.workspace_id, working_directory_id)?
.map(|record| record.runtime_id)
.ok_or_else(|| {
ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: "workspace-backend".to_string(),
code: "working_directory_not_found".to_string(),
message: format!("Unknown Workdir `{working_directory_id}`"),
},
Vec::new(),
)
})
}
fn create_working_directory_for_runtime(
api: WorkspaceApi,
request: BrowserWorkingDirectoryCreateRequest,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
let runtime_id = request.runtime_id.clone();
let mut working_directory_request = working_directory_request_for_browser(&api, request)?;
let workdir_id = next_backend_workdir_id(&working_directory_request.repository.id);
working_directory_request.backend_workdir_id = Some(workdir_id.clone());
let pending = WorkdirRegistryRecord {
workspace_id: api.config.workspace_id.clone(),
workdir_id: workdir_id.clone(),
runtime_id: runtime_id.clone(),
repository_id: working_directory_request.repository.id.clone(),
creation_selector: working_directory_request
.repository
.selector
.as_ref()
.map(|selector| selector.as_ref().to_string()),
creation_ref: None,
current_selector: None,
current_ref: None,
materialization_status: "pending".to_string(),
cleanliness: "unknown".to_string(),
created_at: now_registry_timestamp(),
updated_at: now_registry_timestamp(),
};
api.store.upsert_workdir_registry(&pending)?;
let result = api
.runtime
.create_working_directory(&runtime_id, working_directory_request)
.map_err(|err| err.into_error())?;
let Some(working_directory) = result.working_directory else {
let mut failed = pending;
failed.materialization_status = "failed".to_string();
failed.updated_at = now_registry_timestamp();
api.store.upsert_workdir_registry(&failed)?;
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id,
code: "workspace_working_directory_create_failed".to_string(),
message: "Runtime did not create working directory".to_string(),
},
result.diagnostics,
));
};
let record = workdir_record_from_summary(&api, &runtime_id, &working_directory.summary);
api.store.upsert_workdir_registry(&record)?;
let mut summary = working_directory.summary;
apply_workdir_occupancy_projection(&api, &mut summary)?;
Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item: summary,
diagnostics: result.diagnostics,
}))
}
fn working_directory_detail_for_runtime(
api: WorkspaceApi,
runtime_id: &str,
working_directory_id: &str,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
let result = api
.runtime
.working_directory(runtime_id, working_directory_id)
.map_err(|err| err.into_error())?;
if let Some(working_directory) = result.working_directory {
let record = workdir_record_from_summary(&api, runtime_id, &working_directory.summary);
api.store.upsert_workdir_registry(&record)?;
let mut summary = working_directory.summary;
apply_workdir_occupancy_projection(&api, &mut summary)?;
return Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item: summary,
diagnostics: result.diagnostics,
}));
}
if let Some(record) = api
.store
.get_workdir_registry(&api.config.workspace_id, working_directory_id)?
{
return Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item: projected_workdir_summary_from_record(&api, &record)?,
diagnostics: result.diagnostics,
}));
}
Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: runtime_id.to_string(),
code: "workspace_working_directory_lookup_failed".to_string(),
message: "Runtime did not return working directory".to_string(),
},
result.diagnostics,
))
}
fn cleanup_working_directory_for_runtime(
api: WorkspaceApi,
runtime_id: &str,
working_directory_id: &str,
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
if let Some(candidate) = build_runtime_cleanup_plan(&api, runtime_id)?
.workdirs
.into_iter()
.find(|candidate| candidate.workdir_id == working_directory_id)
{
if let Some(reason) = candidate.blocking_reason {
return Err(cleanup_api_error(
runtime_id,
"workspace_cleanup_workdir_blocked",
&reason,
));
}
if candidate.action == CleanupTargetKind::WorkdirDirtyDiscard {
return Err(cleanup_api_error(
runtime_id,
"workspace_cleanup_dirty_confirmation_required",
"dirty Workdir discard requires the cleanup execution API with explicit confirmation",
));
}
}
let result = api
.runtime
.cleanup_working_directory(runtime_id, working_directory_id)
.map_err(|err| err.into_error())?;
let Some(working_directory) = result.working_directory else {
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: runtime_id.to_string(),
code: "workspace_working_directory_cleanup_failed".to_string(),
message: "Runtime did not cleanup working directory".to_string(),
},
result.diagnostics,
));
};
let record = workdir_record_from_summary(&api, runtime_id, &working_directory.summary);
api.store.upsert_workdir_registry(&record)?;
let mut summary = working_directory.summary;
apply_workdir_occupancy_projection(&api, &mut summary)?;
Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item: summary,
diagnostics: result.diagnostics,
}))
}
async fn set_worker_retention(
api: WorkspaceApi,
runtime_id: String,
runtime_worker_id: String,
pinned: bool,
) -> ApiResult<Json<WorkerRetentionResponse>> {
let runtime_worker_registry_id = parse_runtime_worker_id_for_registry(&runtime_worker_id)?;
if api
.store
.get_worker_registry(
&api.config.workspace_id,
runtime_id.as_str(),
runtime_worker_registry_id,
)?
.is_none()
{
if let Ok(worker) = api
.runtime
.worker(runtime_id.as_str(), runtime_worker_id.as_str())
{
let _ = sync_worker_observation(&api, &worker);
}
}
let retention_state = if pinned { "pinned" } else { "normal" };
let changed = api.store.update_worker_retention(
&api.config.workspace_id,
runtime_id.as_str(),
runtime_worker_registry_id,
retention_state,
now_registry_timestamp().as_str(),
)?;
if !changed {
return Err(cleanup_api_error(
runtime_id.as_str(),
"workspace_worker_retention_unknown_worker",
"Worker is not known to the Backend registry",
));
}
Ok(Json(WorkerRetentionResponse {
workspace_id: api.config.workspace_id,
runtime_id,
worker_id: runtime_worker_id,
pinned,
retention_state: retention_state.to_string(),
}))
}
fn build_runtime_cleanup_plan(
api: &WorkspaceApi,
runtime_id: &str,
) -> ApiResult<RuntimeCleanupPlanResponse> {
let workers = workers_response(api.clone())?;
let live_running_worker_ids: HashSet<(String, u64)> = workers
.items
.iter()
.filter(|worker| worker.state == "running")
.filter_map(|worker| {
parse_runtime_worker_id_for_registry(worker.worker_id.as_str())
.ok()
.map(|worker_id| (worker.runtime_id.clone(), worker_id))
})
.collect();
let (workdir_summaries, mut diagnostics) =
match runtime_working_directory_summaries(api, runtime_id) {
Ok(result) => result,
Err(error) => {
let mut diagnostics = error.diagnostics;
if diagnostics.is_empty() {
diagnostics.push(RuntimeDiagnostic {
code: "workspace_cleanup_runtime_observation_unavailable".to_string(),
severity: DiagnosticSeverity::Warning,
message: sanitize_backend_error(&error.error.to_string()),
});
}
(Vec::new(), diagnostics)
}
};
let workdir_records = api
.store
.list_workdir_registry(&api.config.workspace_id, 500)?;
let worker_records = api
.store
.list_worker_registry(&api.config.workspace_id, 500)?;
let worker_by_id: HashMap<_, _> = worker_records
.iter()
.map(|record| {
(
(record.runtime_id.clone(), record.runtime_worker_id.clone()),
record.clone(),
)
})
.collect();
let observed_workdirs: HashMap<_, _> = workdir_summaries
.into_iter()
.map(|summary| (summary.working_directory_id.clone(), summary))
.collect();
let mut worker_candidates = Vec::new();
for record in worker_records
.iter()
.filter(|record| record.runtime_id == runtime_id)
{
let links = api.store.list_worker_workdir_links(
&api.config.workspace_id,
record.runtime_id.as_str(),
record.runtime_worker_id,
)?;
let is_running = live_running_worker_ids
.contains(&(record.runtime_id.clone(), record.runtime_worker_id.clone()));
let pinned = record.retention_state == "pinned";
let blocking_reason = if pinned {
Some("worker is pinned".to_string())
} else if is_running {
Some("worker is running".to_string())
} else {
None
};
worker_candidates.push(CleanupWorkerCandidate {
target_id: format!(
"worker:{}:{}",
encode_path_segment(record.runtime_id.as_str()),
encode_path_segment(&record.runtime_worker_id.to_string())
),
action: CleanupTargetKind::WorkerDelete,
worker_id: record.runtime_worker_id.to_string(),
runtime_worker_id: record.runtime_worker_id.to_string(),
runtime_id: record.runtime_id.clone(),
reason: if blocking_reason.is_some() {
"Worker cannot be deleted until blocking conditions are cleared".to_string()
} else {
"Stopped or missing Worker can be manually deleted".to_string()
},
blocking_reason,
pinned,
retention_state: record.retention_state.clone(),
linked_workdir_ids: links.iter().map(|link| link.workdir_id.clone()).collect(),
running_linked: is_running,
estimated_reclaim_bytes: None,
});
}
let mut workdir_candidates = Vec::new();
for record in workdir_records
.iter()
.filter(|record| record.runtime_id == runtime_id)
{
let links = api
.store
.list_workdir_worker_links(&api.config.workspace_id, record.workdir_id.as_str())?;
let linked_workers = links
.iter()
.filter_map(|link| {
worker_by_id.get(&(link.runtime_id.clone(), link.runtime_worker_id.clone()))
})
.collect::<Vec<_>>();
let linked_worker_ids = links
.iter()
.map(|link| link.runtime_worker_id.to_string())
.collect::<Vec<_>>();
let linked_running_worker_ids = linked_workers
.iter()
.filter(|worker| {
live_running_worker_ids
.contains(&(worker.runtime_id.clone(), worker.runtime_worker_id.clone()))
})
.map(|worker| worker.runtime_worker_id.to_string())
.collect::<Vec<_>>();
let pinned_linked = linked_workers
.iter()
.any(|worker| worker.retention_state == "pinned");
let running_linked = !linked_running_worker_ids.is_empty();
let observed_status = observed_workdirs
.get(record.workdir_id.as_str())
.map(|summary| CleanupWorkdirFileStatus::from_runtime(&summary.status));
let file_status = observed_status.unwrap_or_else(|| {
CleanupWorkdirFileStatus::from_registry(&record.materialization_status)
});
let cleanliness = observed_workdirs
.get(record.workdir_id.as_str())
.map(|summary| CleanupWorkdirCleanliness::from_runtime(summary.cleanliness.as_deref()))
.unwrap_or_else(|| CleanupWorkdirCleanliness::from_registry(&record.cleanliness));
let action = if file_status.is_record_only() {
CleanupTargetKind::WorkdirRecordDelete
} else if file_status.is_corrupted() || cleanliness.is_clean() {
CleanupTargetKind::WorkdirCleanCleanup
} else {
CleanupTargetKind::WorkdirDirtyDiscard
};
let blocking_reason = if running_linked {
Some("workdir is linked to a running Worker".to_string())
} else if pinned_linked {
Some("workdir is linked to a pinned Worker/history".to_string())
} else {
None
};
workdir_candidates.push(CleanupWorkdirCandidate {
target_id: format!("workdir:{}", record.workdir_id),
action,
workdir_id: record.workdir_id.clone(),
runtime_id: record.runtime_id.clone(),
repository_id: record.repository_id.clone(),
reason: if blocking_reason.is_some() {
"Workdir cleanup is blocked until linked Worker state is safe".to_string()
} else if file_status.is_record_only() {
"Not-found Workdir record can be deleted from the Backend registry".to_string()
} else if file_status.is_corrupted() {
"Corrupted Workdir can be deleted from Runtime storage and Backend registry"
.to_string()
} else if matches!(cleanliness, CleanupWorkdirCleanliness::Dirty) {
"Dirty Workdir requires explicit discard confirmation before cleanup".to_string()
} else if matches!(cleanliness, CleanupWorkdirCleanliness::Unknown) {
"Workdir clean state is unknown; explicit discard confirmation is required"
.to_string()
} else {
"Clean Workdir can be manually cleaned up".to_string()
},
blocking_reason,
linked_worker_ids,
linked_running_worker_ids,
running_linked,
pinned_linked,
file_status,
cleanliness,
estimated_reclaim_bytes: None,
});
}
worker_candidates.sort_by(|left, right| left.target_id.cmp(&right.target_id));
workdir_candidates.sort_by(|left, right| left.target_id.cmp(&right.target_id));
diagnostics.truncate(16);
let generated_at = now_registry_timestamp();
let digest = cleanup_plan_digest(&worker_candidates, &workdir_candidates)?;
Ok(RuntimeCleanupPlanResponse {
workspace_id: api.config.workspace_id.clone(),
runtime_id: runtime_id.to_string(),
generated_at,
revision: digest.clone(),
digest,
workers: worker_candidates,
workdirs: workdir_candidates,
diagnostics,
})
}
fn cleanup_plan_digest(
workers: &[CleanupWorkerCandidate],
workdirs: &[CleanupWorkdirCandidate],
) -> ApiResult<String> {
let bytes = serde_json::to_vec(&(workers, workdirs)).map_err(|error| {
cleanup_api_error(
"backend",
"workspace_cleanup_plan_digest_failed",
&format!("failed to serialize cleanup plan: {error}"),
)
})?;
let mut hasher = Sha256::new();
hasher.update(bytes);
let bytes = hasher.finalize();
let digest = bytes
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
Ok(format!("sha256:{digest}"))
}
fn execute_runtime_cleanup(
api: &WorkspaceApi,
runtime_id: &str,
request: ExecuteRuntimeCleanupRequest,
) -> ApiResult<RuntimeCleanupExecutionResponse> {
let plan = build_runtime_cleanup_plan(api, runtime_id)?;
if request.expected_plan_revision != plan.revision
|| request.expected_plan_digest != plan.digest
{
return Err(cleanup_api_error(
runtime_id,
"workspace_cleanup_plan_stale",
"cleanup plan revision/digest is stale; refresh the preview before executing",
));
}
let worker_targets: HashSet<_> = request.worker_target_ids.iter().cloned().collect();
let workdir_targets: HashSet<_> = request.workdir_target_ids.iter().cloned().collect();
let dirty_confirmations: HashSet<_> = request
.confirm_dirty_discard_target_ids
.iter()
.cloned()
.collect();
let mut results = Vec::new();
for candidate in plan
.workers
.iter()
.filter(|candidate| worker_targets.contains(candidate.target_id.as_str()))
{
if let Some(reason) = &candidate.blocking_reason {
return Err(cleanup_api_error(
runtime_id,
"workspace_cleanup_worker_blocked",
reason,
));
}
if candidate.pinned {
return Err(cleanup_api_error(
runtime_id,
"workspace_cleanup_worker_pinned",
"pinned Worker/history cannot be deleted",
));
}
let runtime_worker_id = parse_runtime_worker_id_for_registry(&candidate.runtime_worker_id)?;
cleanup_runtime_worker_for_execution(api, runtime_id, candidate)?;
api.store.delete_worker_registry(
&api.config.workspace_id,
candidate.runtime_id.as_str(),
runtime_worker_id,
)?;
results.push(RuntimeCleanupExecutionResult {
target_id: candidate.target_id.clone(),
action: candidate.action.clone(),
status: "deleted".to_string(),
message: "Worker deleted from Runtime and Backend registry".to_string(),
});
}
for candidate in plan
.workdirs
.iter()
.filter(|candidate| workdir_targets.contains(candidate.target_id.as_str()))
{
if let Some(reason) = &candidate.blocking_reason {
return Err(cleanup_api_error(
runtime_id,
"workspace_cleanup_workdir_blocked",
reason,
));
}
match candidate.action {
CleanupTargetKind::WorkdirDirtyDiscard => {
if !dirty_confirmations.contains(candidate.target_id.as_str()) {
return Err(cleanup_api_error(
runtime_id,
"workspace_cleanup_dirty_confirmation_required",
"dirty Workdir discard requires explicit confirmation",
));
}
cleanup_runtime_workdir_for_execution(api, runtime_id, candidate)?;
let deleted = api.store.delete_workdir_registry(
&api.config.workspace_id,
candidate.workdir_id.as_str(),
)?;
if !deleted {
return Err(cleanup_api_error(
runtime_id,
"workspace_cleanup_workdir_registry_not_found",
"Backend Workdir registry row was not found after Runtime cleanup",
));
}
results.push(RuntimeCleanupExecutionResult {
target_id: candidate.target_id.clone(),
action: candidate.action.clone(),
status: "deleted".to_string(),
message:
"Dirty/unknown Workdir was deleted from Runtime storage and Backend registry after explicit confirmation"
.to_string(),
});
}
CleanupTargetKind::WorkdirCleanCleanup => {
cleanup_runtime_workdir_for_execution(api, runtime_id, candidate)?;
let deleted = api.store.delete_workdir_registry(
&api.config.workspace_id,
candidate.workdir_id.as_str(),
)?;
if !deleted {
return Err(cleanup_api_error(
runtime_id,
"workspace_cleanup_workdir_registry_not_found",
"Backend Workdir registry row was not found after Runtime cleanup",
));
}
results.push(RuntimeCleanupExecutionResult {
target_id: candidate.target_id.clone(),
action: candidate.action.clone(),
status: "deleted".to_string(),
message: "Workdir deleted from Runtime storage and Backend registry"
.to_string(),
});
}
CleanupTargetKind::WorkdirRecordDelete => {
let deleted = api.store.delete_workdir_registry(
&api.config.workspace_id,
candidate.workdir_id.as_str(),
)?;
if !deleted {
return Err(cleanup_api_error(
runtime_id,
"workspace_cleanup_workdir_registry_not_found",
"Backend Workdir registry row was not found",
));
}
results.push(RuntimeCleanupExecutionResult {
target_id: candidate.target_id.clone(),
action: candidate.action.clone(),
status: "deleted".to_string(),
message: "Not-found Workdir registry row deleted".to_string(),
});
}
CleanupTargetKind::WorkerDelete => {
return Err(cleanup_api_error(
runtime_id,
"workspace_cleanup_invalid_target_kind",
"worker delete action cannot be executed as a Workdir target",
));
}
}
}
let requested_target_count = worker_targets.len() + workdir_targets.len();
if requested_target_count > 0 && results.len() != requested_target_count {
return Err(cleanup_api_error(
runtime_id,
"workspace_cleanup_target_not_executed",
"one or more selected cleanup targets were not present in the current cleanup plan",
));
}
let plan_after = build_runtime_cleanup_plan(api, runtime_id)?;
let executed_at = now_registry_timestamp();
Ok(RuntimeCleanupExecutionResponse {
workspace_id: api.config.workspace_id.clone(),
runtime_id: runtime_id.to_string(),
executed_at,
results,
diagnostics: plan_after.diagnostics.clone(),
plan_after,
})
}
fn cleanup_runtime_worker_for_execution(
api: &WorkspaceApi,
runtime_id: &str,
candidate: &CleanupWorkerCandidate,
) -> ApiResult<()> {
match api.runtime.stop_worker(
runtime_id,
candidate.runtime_worker_id.as_str(),
WorkerLifecycleRequest {
reason: Some("cleanup worker before deletion".to_string()),
ticket_assignment: None,
},
) {
Ok(result) if result.state == WorkerOperationState::Accepted => {}
Ok(result) => {
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: runtime_id.to_string(),
code: "workspace_cleanup_worker_runtime_stop_rejected".to_string(),
message: "Runtime did not stop selected Worker before deletion".to_string(),
},
result.diagnostics,
));
}
Err(RuntimeRegistryError::UnknownWorker { .. }) => return Ok(()),
Err(error) => return Err(error.into_error().into()),
}
match api
.runtime
.delete_worker(runtime_id, candidate.runtime_worker_id.as_str())
{
Ok(result) if result.deleted && result.state == WorkerOperationState::Accepted => Ok(()),
Ok(result) => Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: runtime_id.to_string(),
code: "workspace_cleanup_worker_runtime_delete_rejected".to_string(),
message: "Runtime did not delete selected Worker after stopping it".to_string(),
},
result.diagnostics,
)),
Err(RuntimeRegistryError::UnknownWorker { .. }) => Ok(()),
Err(error) => Err(error.into_error().into()),
}
}
fn cleanup_runtime_workdir_for_execution(
api: &WorkspaceApi,
runtime_id: &str,
candidate: &CleanupWorkdirCandidate,
) -> ApiResult<()> {
let result = api
.runtime
.cleanup_working_directory(runtime_id, candidate.workdir_id.as_str())
.map_err(|err| err.into_error())?;
if result.working_directory.is_none() {
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: runtime_id.to_string(),
code: "workspace_cleanup_workdir_runtime_failed".to_string(),
message: "Runtime did not cleanup selected Workdir".to_string(),
},
result.diagnostics,
));
};
Ok(())
}
fn cleanup_api_error(runtime_id: &str, code: &str, message: &str) -> ApiError {
Error::RuntimeOperationFailed {
runtime_id: runtime_id.to_string(),
code: code.to_string(),
message: message.to_string(),
}
.into()
}
async fn scoped_get_runtime_connection_settings(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<RuntimeConnectionSettingsResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_runtime_connection_settings(State(api)).await
}
async fn scoped_add_remote_runtime_connection(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(request): Json<AddRemoteRuntimeConnectionRequest>,
) -> ApiResult<Json<RuntimeConnectionMutationResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
add_remote_runtime_connection(State(api), Json(request)).await
}
async fn scoped_delete_remote_runtime_connection(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
) -> ApiResult<Json<RuntimeConnectionMutationResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
delete_remote_runtime_connection(State(api), AxumPath(path.runtime_id)).await
}
async fn scoped_test_remote_runtime_connection(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
) -> ApiResult<Json<RemoteRuntimeTestResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
test_remote_runtime_connection(State(api), AxumPath(path.runtime_id)).await
}
async fn scoped_get_companion_status(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<CompanionStatusResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_companion_status(State(api)).await
}
async fn scoped_get_companion_transcript(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Query(query): Query<TranscriptQuery>,
) -> ApiResult<Json<CompanionTranscriptProjection>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_companion_transcript(State(api), Query(query)).await
}
async fn scoped_post_companion_message(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(request): Json<CompanionMessageRequest>,
) -> ApiResult<Json<CompanionMessageResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
post_companion_message(State(api), Json(request)).await
}
async fn scoped_post_companion_cancel(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Json(request): Json<CompanionCancelRequest>,
) -> ApiResult<Json<CompanionMessageResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
post_companion_cancel(State(api), Json(request)).await
}
async fn scoped_list_runtime_workers(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Query(query): Query<RuntimeWorkersQuery>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_runtime_workers(State(api), AxumPath(path.runtime_id), Query(query)).await
}
async fn scoped_create_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Json(request): Json<WorkerSpawnRequest>,
) -> ApiResult<Json<WorkerSpawnResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
create_runtime_worker(State(api), AxumPath(path.runtime_id), Json(request)).await
}
async fn scoped_sync_runtime_config_bundle(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Json(request): Json<RuntimeConfigBundleSyncRequest>,
) -> ApiResult<Json<ConfigBundleSyncResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
sync_runtime_config_bundle(State(api), AxumPath(path.runtime_id), Json(request)).await
}
async fn scoped_check_runtime_config_bundle(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedConfigBundlePath>,
Query(query): Query<RuntimeConfigBundleAvailabilityQuery>,
) -> ApiResult<Json<ConfigBundleCheckResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
check_runtime_config_bundle(
State(api),
AxumPath((path.runtime_id, path.bundle_id)),
Query(query),
)
.await
}
async fn scoped_get_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
) -> ApiResult<Json<WorkerSummary>> {
validate_workspace_scope(&api, &path.workspace_id)?;
get_runtime_worker(State(api), AxumPath((path.runtime_id, path.worker_id))).await
}
#[derive(Debug, Default, Deserialize)]
struct RestoreTicketAssignmentQuery {
ticket_id: Option<String>,
assignment_operation_id: Option<String>,
}
async fn scoped_restore_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
Query(query): Query<RestoreTicketAssignmentQuery>,
) -> ApiResult<Json<WorkerRestoreResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let workspace_id = path.workspace_id.clone();
let runtime_id = path.runtime_id.clone();
let worker_id = path.worker_id.clone();
let assignment_request = match (
query.ticket_id.clone(),
query.assignment_operation_id.clone(),
) {
(Some(ticket_id), Some(operation_id)) => {
Some(crate::hosts::WorkerTicketAssignmentRequest {
ticket_id,
operation_id,
})
}
(None, None) => None,
_ => {
return Err(Error::TicketAssignmentConflict(
"restore assignment requires both ticket_id and assignment_operation_id"
.to_string(),
)
.into());
}
};
if let Some(assignment) = assignment_request.as_ref() {
let fingerprint = format!(
"sha256:{}",
Sha256::digest(format!(
"restore\0{}\0{}\0{}",
assignment.ticket_id, runtime_id, worker_id
))
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
);
api.store.reserve_ticket_assignment_operation(
&workspace_id,
&assignment.operation_id,
&assignment.ticket_id,
&runtime_id,
Some(&worker_id),
&fingerprint,
&Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
)?;
if let Some(worker) = existing_lifecycle_assignment_worker(&api, assignment, &runtime_id)? {
if worker.worker_id != worker_id {
return Err(Error::TicketAssignmentConflict(format!(
"assignment operation {} belongs to worker {}, not {}",
assignment.operation_id, worker.worker_id, worker_id
))
.into());
}
assign_ticket_worker_from_lifecycle(&api, assignment, &runtime_id, &worker_id)?;
return Ok(Json(WorkerRestoreResponse {
workspace_id,
runtime_id,
worker_id,
result: crate::hosts::WorkerRestoreResult {
state: WorkerOperationState::Accepted,
worker: Some(worker),
diagnostics: Vec::new(),
},
}));
}
}
let response = restore_runtime_worker(
State(api.clone()),
AxumPath((runtime_id.clone(), worker_id.clone())),
)
.await?;
if let Some(assignment) = assignment_request.as_ref() {
assign_ticket_worker_from_lifecycle(&api, assignment, &runtime_id, &worker_id)?;
}
Ok(response)
}
async fn scoped_pin_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
) -> ApiResult<Json<WorkerRetentionResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
set_worker_retention(api, path.runtime_id, path.worker_id, true).await
}
async fn scoped_unpin_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
) -> ApiResult<Json<WorkerRetentionResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
set_worker_retention(api, path.runtime_id, path.worker_id, false).await
}
async fn scoped_runtime_cleanup_plan(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
) -> ApiResult<Json<RuntimeCleanupPlanResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let plan = build_runtime_cleanup_plan(&api, path.runtime_id.as_str())?;
Ok(Json(plan))
}
async fn scoped_execute_runtime_cleanup(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Json(request): Json<ExecuteRuntimeCleanupRequest>,
) -> ApiResult<Json<RuntimeCleanupExecutionResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let response = execute_runtime_cleanup(&api, path.runtime_id.as_str(), request)?;
Ok(Json(response))
}
async fn scoped_send_runtime_worker_input(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
Json(request): Json<WorkerInputRequest>,
) -> ApiResult<Json<WorkerInputResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
send_runtime_worker_input(
State(api),
AxumPath((path.runtime_id, path.worker_id)),
Json(request),
)
.await
}
async fn scoped_runtime_worker_completions(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
Json(request): Json<WorkerCompletionsRequest>,
) -> ApiResult<Json<WorkerCompletionsResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
runtime_worker_completions(
State(api),
AxumPath((path.runtime_id, path.worker_id)),
Json(request),
)
.await
}
async fn scoped_stop_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
Json(request): Json<WorkerLifecycleRequest>,
) -> ApiResult<Json<WorkerLifecycleResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
stop_runtime_worker(
State(api),
AxumPath((path.runtime_id, path.worker_id)),
Json(request),
)
.await
}
async fn scoped_cancel_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
Json(request): Json<WorkerLifecycleRequest>,
) -> ApiResult<Json<WorkerLifecycleResult>> {
validate_workspace_scope(&api, &path.workspace_id)?;
cancel_runtime_worker(
State(api),
AxumPath((path.runtime_id, path.worker_id)),
Json(request),
)
.await
}
async fn scoped_worker_protocol_ws(
ws: WebSocketUpgrade,
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
) -> Response {
if let Err(err) = validate_workspace_scope(&api, &path.workspace_id) {
return err.into_response();
}
worker_protocol_ws(State(api), AxumPath((path.runtime_id, path.worker_id)), ws)
.await
.into_response()
}
async fn scoped_list_host_workers(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedHostPath>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?;
list_host_workers(State(api), AxumPath(path.host_id)).await
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct AuthBootstrapUserRequest {
handle: String,
#[serde(default)]
display_name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct AuthUserResponse {
user: AuthenticatedUser,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PasskeyRegistrationOptionsRequest {
handle: String,
#[serde(default)]
display_name: Option<String>,
#[serde(default)]
browser_origin: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct PasskeyRegistrationOptionsResponse {
challenge_id: String,
public_key: CreationChallengeResponse,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PasskeyRegistrationCompleteRequest {
challenge_id: String,
credential: RegisterPublicKeyCredential,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PasskeyLoginOptionsRequest {
#[serde(default)]
handle: Option<String>,
#[serde(default)]
browser_origin: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct PasskeyLoginOptionsResponse {
challenge_id: String,
public_key: RequestChallengeResponse,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PasskeyLoginCompleteRequest {
challenge_id: String,
credential: PublicKeyCredential,
}
#[derive(Debug, Serialize, Deserialize)]
struct PasskeyLoginCompleteResponse {
user: AuthenticatedUser,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct DeviceLoginStartRequest {
#[serde(default)]
client_name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct DeviceLoginStartResponse {
device_code: String,
user_code: String,
verification_uri: String,
verification_uri_complete: String,
expires_in: u64,
interval: u64,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct DeviceLoginApproveRequest {
user_code: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct DeviceLoginApproveResponse {
status: String,
user: AuthenticatedUser,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct DeviceLoginPollRequest {
device_code: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct DeviceLoginPollResponse {
status: String,
#[serde(skip_serializing_if = "Option::is_none")]
access_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
token_type: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct WhoamiResponse {
actor: Option<RequestActor>,
}
#[derive(Debug, Serialize, Deserialize)]
struct LogoutResponse {
status: String,
}
async fn get_auth_config(State(api): State<WorkspaceApi>) -> ApiResult<Json<AuthPublicConfig>> {
Ok(Json(auth_public_config(&api.config)))
}
async fn post_auth_bootstrap_user(
State(api): State<WorkspaceApi>,
Json(request): Json<AuthBootstrapUserRequest>,
) -> ApiResult<Json<AuthUserResponse>> {
let user = ensure_user_account(&api, &request.handle, request.display_name.as_deref())?;
Ok(Json(AuthUserResponse { user }))
}
async fn post_passkey_registration_options(
State(api): State<WorkspaceApi>,
headers: HeaderMap,
Json(request): Json<PasskeyRegistrationOptionsRequest>,
) -> ApiResult<Json<PasskeyRegistrationOptionsResponse>> {
let user = ensure_user_account(&api, &request.handle, request.display_name.as_deref())?;
let header_origin = request_origin(&headers);
let requested_origin = request
.browser_origin
.as_deref()
.or(header_origin.as_deref());
let auth = auth_config_for_origin(&api.config, requested_origin)?;
let webauthn = webauthn_for_auth(&auth)?;
let exclude_credentials = passkeys_for_user(&api, &user.user_id)?
.into_iter()
.map(|passkey| passkey.cred_id().clone())
.collect();
let user_unique_id = Uuid::now_v7();
let (public_key, state) = webauthn
.start_passkey_registration(
user_unique_id,
&user.handle,
&user.display_name,
Some(exclude_credentials),
)
.map_err(|error| auth_error("webauthn_registration_options_failed", &error.to_string()))?;
let challenge_id = new_id("webauthn-registration");
api.store.put_auth_challenge(&AuthChallengeRecord {
challenge_id: challenge_id.clone(),
ceremony: "passkey_registration".to_string(),
challenge: challenge_id.clone(),
user_id: Some(user.user_id),
rp_id: auth.rp_id,
origin: auth.origin,
state_json: Some(
serde_json::to_string(&state).map_err(|error| {
auth_error("webauthn_state_serialize_failed", &error.to_string())
})?,
),
expires_at: rfc3339_after(Duration::minutes(5)),
created_at: crate::auth::now_rfc3339(),
consumed_at: None,
})?;
Ok(Json(PasskeyRegistrationOptionsResponse {
challenge_id,
public_key,
}))
}
async fn post_passkey_registration_complete(
State(api): State<WorkspaceApi>,
Json(request): Json<PasskeyRegistrationCompleteRequest>,
) -> ApiResult<Response> {
let challenge = api
.store
.consume_auth_challenge_by_id(
&request.challenge_id,
"passkey_registration",
&crate::auth::now_rfc3339(),
)?
.ok_or_else(|| {
auth_error(
"invalid_passkey_challenge",
"passkey registration challenge is invalid or already consumed",
)
})?;
if is_expired(&challenge.expires_at) {
return Err(auth_error(
"expired_passkey_challenge",
"passkey registration challenge expired",
)
.into());
}
let user_id = challenge.user_id.clone().ok_or_else(|| {
auth_error(
"invalid_passkey_challenge",
"passkey registration challenge is not bound to a user",
)
})?;
let user = api.store.get_user(&user_id)?.ok_or_else(|| {
auth_error(
"unknown_auth_user",
"passkey registration user does not exist",
)
})?;
let state_json = challenge.state_json.clone().ok_or_else(|| {
auth_error(
"missing_webauthn_state",
"passkey registration state was not persisted",
)
})?;
let state: PasskeyRegistration = serde_json::from_str(&state_json)
.map_err(|error| auth_error("webauthn_state_deserialize_failed", &error.to_string()))?;
let webauthn = webauthn_for_challenge(&api.config, &challenge)?;
let passkey = webauthn
.finish_passkey_registration(&request.credential, &state)
.map_err(|error| {
auth_error(
"webauthn_registration_verification_failed",
&error.to_string(),
)
})?;
let credential_id = passkey_credential_id(&passkey)?;
api.store
.upsert_passkey_credential(&PasskeyCredentialRecord {
credential_id,
user_id: user.user_id.clone(),
public_key_cose: serde_json::to_string(&passkey).map_err(|error| {
auth_error("webauthn_passkey_serialize_failed", &error.to_string())
})?,
transports_json: None,
sign_count: 0,
created_at: crate::auth::now_rfc3339(),
last_used_at: None,
})?;
issue_browser_session_response(&api, user)
}
async fn post_passkey_login_options(
State(api): State<WorkspaceApi>,
headers: HeaderMap,
Json(request): Json<PasskeyLoginOptionsRequest>,
) -> ApiResult<Json<PasskeyLoginOptionsResponse>> {
let user = match request.handle.as_deref() {
Some(handle) => api.store.get_user_by_handle(&normalize_handle(handle)?)?,
None => api.store.any_user()?,
}
.ok_or_else(|| auth_error("unknown_auth_user", "no matching user account exists"))?;
let passkeys = passkeys_for_user(&api, &user.user_id)?;
if passkeys.is_empty() {
return Err(auth_error(
"passkey_not_registered",
"user has no registered passkey credentials",
)
.into());
}
let header_origin = request_origin(&headers);
let requested_origin = request
.browser_origin
.as_deref()
.or(header_origin.as_deref());
let auth = auth_config_for_origin(&api.config, requested_origin)?;
let webauthn = webauthn_for_auth(&auth)?;
let (public_key, state) = webauthn
.start_passkey_authentication(&passkeys)
.map_err(|error| auth_error("webauthn_login_options_failed", &error.to_string()))?;
let challenge_id = new_id("webauthn-login");
api.store.put_auth_challenge(&AuthChallengeRecord {
challenge_id: challenge_id.clone(),
ceremony: "passkey_login".to_string(),
challenge: challenge_id.clone(),
user_id: Some(user.user_id),
rp_id: auth.rp_id,
origin: auth.origin,
state_json: Some(
serde_json::to_string(&state).map_err(|error| {
auth_error("webauthn_state_serialize_failed", &error.to_string())
})?,
),
expires_at: rfc3339_after(Duration::minutes(5)),
created_at: crate::auth::now_rfc3339(),
consumed_at: None,
})?;
Ok(Json(PasskeyLoginOptionsResponse {
challenge_id,
public_key,
}))
}
async fn post_passkey_login_complete(
State(api): State<WorkspaceApi>,
Json(request): Json<PasskeyLoginCompleteRequest>,
) -> ApiResult<Response> {
let challenge = api
.store
.consume_auth_challenge_by_id(
&request.challenge_id,
"passkey_login",
&crate::auth::now_rfc3339(),
)?
.ok_or_else(|| {
auth_error(
"invalid_passkey_challenge",
"passkey login challenge is invalid or already consumed",
)
})?;
if is_expired(&challenge.expires_at) {
return Err(auth_error(
"expired_passkey_challenge",
"passkey login challenge expired",
)
.into());
}
let user_id = challenge.user_id.clone().ok_or_else(|| {
auth_error(
"invalid_passkey_challenge",
"passkey login challenge is not bound to a user",
)
})?;
let user = api
.store
.get_user(&user_id)?
.ok_or_else(|| auth_error("unknown_auth_user", "passkey user does not exist"))?;
let state_json = challenge.state_json.clone().ok_or_else(|| {
auth_error(
"missing_webauthn_state",
"passkey login state was not persisted",
)
})?;
let state: PasskeyAuthentication = serde_json::from_str(&state_json)
.map_err(|error| auth_error("webauthn_state_deserialize_failed", &error.to_string()))?;
let webauthn = webauthn_for_challenge(&api.config, &challenge)?;
let auth_result = webauthn
.finish_passkey_authentication(&request.credential, &state)
.map_err(|error| auth_error("webauthn_login_verification_failed", &error.to_string()))?;
let credential_id = serde_json::to_value(auth_result.cred_id())
.ok()
.and_then(|value| value.as_str().map(ToOwned::to_owned))
.ok_or_else(|| {
auth_error(
"invalid_webauthn_credential_id",
"verified credential id was not serializable",
)
})?;
let stored = api
.store
.get_passkey_credential(&credential_id)?
.ok_or_else(|| {
auth_error(
"unknown_passkey_credential",
"verified passkey credential is not registered",
)
})?;
if stored.user_id != user.user_id {
return Err(auth_error(
"passkey_user_mismatch",
"verified passkey credential does not belong to the challenged user",
)
.into());
}
api.store
.upsert_passkey_credential(&PasskeyCredentialRecord {
credential_id,
user_id: user.user_id.clone(),
public_key_cose: stored.public_key_cose,
transports_json: stored.transports_json,
sign_count: u64::from(auth_result.counter()),
created_at: stored.created_at,
last_used_at: Some(crate::auth::now_rfc3339()),
})?;
issue_browser_session_response(&api, user)
}
fn issue_browser_session_response(api: &WorkspaceApi, user: UserRecord) -> ApiResult<Response> {
let session_token = mint_secret("yoi_sess");
api.store.create_browser_session(&BrowserSessionRecord {
session_id: new_id("session"),
token_hash: token_hash(&session_token),
user_id: user.user_id.clone(),
created_at: crate::auth::now_rfc3339(),
expires_at: rfc3339_after(Duration::days(14)),
revoked_at: None,
})?;
let cookie_name = auth_public_config(&api.config).cookie_name;
let mut headers = HeaderMap::new();
headers.insert(
SET_COOKIE,
session_set_cookie(&cookie_name, &session_token, 14 * 24 * 60 * 60)
.parse()
.map_err(|error| {
auth_error(
"invalid_session_cookie",
&format!("failed to build session cookie: {error}"),
)
})?,
);
Ok((
headers,
Json(AuthUserResponse {
user: user_response(user),
}),
)
.into_response())
}
async fn post_device_login_start(
State(api): State<WorkspaceApi>,
Json(request): Json<DeviceLoginStartRequest>,
) -> ApiResult<Json<DeviceLoginStartResponse>> {
let auth = auth_public_config(&api.config);
let device_code = mint_secret("yoi_device");
let user_code = new_user_code();
let verification_uri = format!(
"{}/login/device",
auth.public_base_url.trim_end_matches('/')
);
let verification_uri_complete = format!("{verification_uri}?user_code={user_code}");
api.store.create_device_login_flow(&DeviceLoginFlowRecord {
device_code: device_code.clone(),
user_code: user_code.clone(),
verification_uri: verification_uri.clone(),
client_name: request.client_name,
user_id: None,
api_token_id: None,
issued_access_token: None,
created_at: crate::auth::now_rfc3339(),
expires_at: rfc3339_after(Duration::minutes(10)),
approved_at: None,
consumed_at: None,
})?;
Ok(Json(DeviceLoginStartResponse {
device_code,
user_code,
verification_uri,
verification_uri_complete,
expires_in: 600,
interval: 5,
}))
}
async fn post_device_login_approve(
State(api): State<WorkspaceApi>,
headers: HeaderMap,
Json(request): Json<DeviceLoginApproveRequest>,
) -> ApiResult<Json<DeviceLoginApproveResponse>> {
let actor = require_actor(&api, &headers).await?;
let flow = api
.store
.get_device_login_flow_by_user_code(&request.user_code.trim().to_ascii_uppercase())?
.ok_or_else(|| {
auth_error(
"unknown_device_login_code",
"device login code does not exist",
)
})?;
if is_expired(&flow.expires_at) {
return Err(auth_error("expired_device_login", "device login code expired").into());
}
if flow.approved_at.is_some() || flow.consumed_at.is_some() {
return Err(auth_error(
"device_login_already_used",
"device login code is already used",
)
.into());
}
let access_token = mint_secret("yoi_api");
let token_id = new_id("api-token");
api.store.create_api_token(&ApiTokenRecord {
token_id: token_id.clone(),
token_hash: token_hash(&access_token),
user_id: actor.user_id.clone(),
label: flow
.client_name
.clone()
.unwrap_or_else(|| "yoi device login".to_string()),
created_at: crate::auth::now_rfc3339(),
expires_at: None,
revoked_at: None,
last_used_at: None,
})?;
let approved = api.store.approve_device_login_flow(
&flow.device_code,
&actor.user_id,
&token_id,
&access_token,
&crate::auth::now_rfc3339(),
)?;
if !approved {
return Err(auth_error(
"device_login_already_used",
"device login code is already used",
)
.into());
}
Ok(Json(DeviceLoginApproveResponse {
status: "approved".to_string(),
user: actor.user(),
}))
}
async fn post_device_login_poll(
State(api): State<WorkspaceApi>,
Json(request): Json<DeviceLoginPollRequest>,
) -> ApiResult<Json<DeviceLoginPollResponse>> {
let Some(flow) = api
.store
.get_device_login_flow_by_device_code(&request.device_code)?
else {
return Err(auth_error("unknown_device_login", "device login flow does not exist").into());
};
if is_expired(&flow.expires_at) {
return Ok(Json(DeviceLoginPollResponse {
status: "expired".to_string(),
access_token: None,
token_type: None,
}));
}
if flow.approved_at.is_none() {
return Ok(Json(DeviceLoginPollResponse {
status: "pending".to_string(),
access_token: None,
token_type: None,
}));
}
let Some(consumed) = api
.store
.consume_device_login_token(&request.device_code, &crate::auth::now_rfc3339())?
else {
return Ok(Json(DeviceLoginPollResponse {
status: "consumed".to_string(),
access_token: None,
token_type: None,
}));
};
Ok(Json(DeviceLoginPollResponse {
status: "approved".to_string(),
access_token: consumed.issued_access_token,
token_type: Some("Bearer".to_string()),
}))
}
async fn get_auth_whoami(
State(api): State<WorkspaceApi>,
headers: HeaderMap,
) -> ApiResult<Json<WhoamiResponse>> {
Ok(Json(WhoamiResponse {
actor: resolve_actor(&api, &headers).await?,
}))
}
async fn post_auth_logout(
State(api): State<WorkspaceApi>,
headers: HeaderMap,
) -> ApiResult<Response> {
let auth = auth_public_config(&api.config);
if let Some(session_token) = parse_cookie(&headers, &auth.cookie_name) {
let _ = api
.store
.revoke_browser_session(&token_hash(&session_token), &crate::auth::now_rfc3339())?;
}
let mut response_headers = HeaderMap::new();
response_headers.insert(
SET_COOKIE,
session_set_cookie(&auth.cookie_name, "", 0)
.parse()
.map_err(|error| {
auth_error(
"invalid_session_cookie",
&format!("failed to build logout cookie: {error}"),
)
})?,
);
Ok((
response_headers,
Json(LogoutResponse {
status: "logged_out".to_string(),
}),
)
.into_response())
}
fn request_origin(headers: &HeaderMap) -> Option<String> {
headers
.get(ORIGIN)
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn auth_config_for_origin(
config: &ServerConfig,
request_origin: Option<&str>,
) -> ApiResult<AuthPublicConfig> {
let mut auth = auth_public_config(config);
let Some(request_origin) = request_origin else {
return Ok(auth);
};
let origin = Url::parse(request_origin)
.map_err(|error| auth_error("invalid_webauthn_request_origin", &error.to_string()))?;
let host = origin.host_str().ok_or_else(|| {
auth_error(
"invalid_webauthn_request_origin",
"request Origin header does not contain a host",
)
})?;
if host != auth.rp_id {
return Err(auth_error(
"webauthn_origin_rp_id_mismatch",
&format!(
"browser origin host {host} does not match configured RP ID {}",
auth.rp_id
),
)
.into());
}
auth.origin = request_origin.to_string();
Ok(auth)
}
fn webauthn_for_challenge(
config: &ServerConfig,
challenge: &AuthChallengeRecord,
) -> ApiResult<Webauthn> {
let mut auth = auth_public_config(config);
auth.rp_id = challenge.rp_id.clone();
auth.origin = challenge.origin.clone();
webauthn_for_auth(&auth)
}
fn webauthn_for_auth(auth: &AuthPublicConfig) -> ApiResult<Webauthn> {
let origin = Url::parse(&auth.origin)
.map_err(|error| auth_error("invalid_webauthn_origin", &error.to_string()))?;
WebauthnBuilder::new(&auth.rp_id, &origin)
.map_err(|error| auth_error("webauthn_builder_failed", &error.to_string()))?
.rp_name("Yoi Workspace")
.build()
.map_err(|error| auth_error("webauthn_builder_failed", &error.to_string()).into())
}
fn passkeys_for_user(api: &WorkspaceApi, user_id: &str) -> ApiResult<Vec<Passkey>> {
api.store
.list_passkey_credentials_for_user(user_id)?
.into_iter()
.map(|record| {
serde_json::from_str::<Passkey>(&record.public_key_cose).map_err(|error| {
auth_error("webauthn_passkey_deserialize_failed", &error.to_string()).into()
})
})
.collect()
}
fn passkey_credential_id(passkey: &Passkey) -> ApiResult<String> {
serde_json::to_value(passkey.cred_id())
.ok()
.and_then(|value| value.as_str().map(ToOwned::to_owned))
.ok_or_else(|| {
auth_error(
"invalid_webauthn_credential_id",
"verified passkey credential id was not serializable",
)
.into()
})
}
fn auth_public_config(config: &ServerConfig) -> AuthPublicConfig {
match &config.auth {
AuthConfig::Passkey {
rp_id,
origin,
public_base_url,
cookie_name,
} => AuthPublicConfig {
rp_id: rp_id.clone(),
origin: origin.clone(),
public_base_url: public_base_url.clone(),
cookie_name: cookie_name.clone(),
},
}
}
fn ensure_user_account(
api: &WorkspaceApi,
handle: &str,
display_name: Option<&str>,
) -> ApiResult<AuthenticatedUser> {
let handle = normalize_handle(handle)?;
if let Some(user) = api.store.get_user_by_handle(&handle)? {
return Ok(user_response(user));
}
let now = crate::auth::now_rfc3339();
let display_name = display_name
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(handle.as_str())
.to_string();
let account = AccountRecord {
account_id: new_id("acct-user"),
kind: "user".to_string(),
handle: handle.clone(),
display_name: display_name.clone(),
created_at: now.clone(),
updated_at: now.clone(),
};
api.store.upsert_account(&account)?;
let user = UserRecord {
user_id: new_id("user"),
account_id: account.account_id,
handle,
display_name,
created_at: now.clone(),
updated_at: now,
};
api.store.upsert_user(&user)?;
Ok(user_response(user))
}
fn user_response(user: UserRecord) -> AuthenticatedUser {
AuthenticatedUser {
user_id: user.user_id,
account_id: user.account_id,
handle: user.handle,
display_name: user.display_name,
}
}
async fn resolve_actor(api: &WorkspaceApi, headers: &HeaderMap) -> ApiResult<Option<RequestActor>> {
let cookie_name = auth_public_config(&api.config).cookie_name;
Ok(resolve_request_actor(api.store.as_ref(), headers, &cookie_name).await?)
}
async fn require_actor(api: &WorkspaceApi, headers: &HeaderMap) -> ApiResult<RequestActor> {
resolve_actor(api, headers).await?.ok_or_else(|| {
auth_error(
"auth_required",
"request requires a browser session or Bearer API token",
)
.into()
})
}
async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<WorkspaceResponse>> {
let schema_version = api.store.schema_version().await?;
let stored = api.store.get_workspace(api.workspace_id()).await?;
let display_name = stored
.as_ref()
.map(|record| record.display_name.clone())
.unwrap_or_else(|| api.config.workspace_display_name.clone());
let companion_status = api.companion.status();
let companion_console = companion_console_extension_point(&companion_status);
Ok(Json(WorkspaceResponse {
workspace_id: api.config.workspace_id.clone(),
display_name,
record_authority: "local_yoi_project_records".to_string(),
schema_version,
auth: api.config.auth.clone(),
extension_points: ExtensionPoints {
store: "sqlite".to_string(),
event_stream: ExtensionPointState {
status: "backend_proxy".to_string(),
note: "Worker observation streams are exposed only through the Workspace server proxy keyed by runtime_id + worker_id; browser clients never receive raw Runtime endpoints or socket paths.".to_string(),
diagnostics: Vec::new(),
},
host_worker_bridge: ExtensionPointState {
status: "runtime_registry".to_string(),
note: "Hosts and Workers are projected from the Workspace RuntimeRegistry; raw Runtime endpoints, sockets, and local metadata paths are not exposed.".to_string(),
diagnostics: Vec::new(),
},
companion_console,
},
}))
}
fn companion_console_extension_point(status: &CompanionStatusResponse) -> ExtensionPointState {
let completion = status.transport.completion.clone();
let note = match completion.as_str() {
"connected" => "Workspace Companion is input-capable and browser input is dispatched through the normal Worker runtime path.".to_string(),
"not_input_capable" => {
let diagnostic_codes = status
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect::<Vec<_>>()
.join(", ");
if diagnostic_codes.is_empty() {
"Workspace Companion is not input-capable; check provider, config, profile, secret, and authority diagnostics.".to_string()
} else {
format!(
"Workspace Companion is not input-capable; check typed diagnostics: {diagnostic_codes}."
)
}
}
"disabled" => "Workspace Companion auto-start has been removed; create an explicit Worker instead.".to_string(),
other => format!(
"Workspace Companion transport reports {other}; browser input follows the Companion Worker runtime capability state."
),
};
ExtensionPointState {
status: completion,
note,
diagnostics: status.diagnostics.clone(),
}
}
async fn list_tickets(
State(api): State<WorkspaceApi>,
Query(query): Query<TicketListQuery>,
) -> ApiResult<Json<crate::records::TicketListResponse>> {
let requested_limit = query.limit.unwrap_or(api.config.max_records);
let limit = requested_limit.min(1000);
let ProjectRecordList {
items,
invalid_records,
record_authority,
} = api.authority.list_tickets(limit)?;
Ok(Json(crate::records::TicketListResponse {
workspace_id: api.config.workspace_id,
limit,
items,
invalid_records,
record_authority,
}))
}
async fn get_ticket(
State(api): State<WorkspaceApi>,
AxumPath(id): AxumPath<String>,
) -> ApiResult<Json<TicketDetail>> {
Ok(Json(api.authority.ticket(&id)?))
}
async fn list_objectives(
State(api): State<WorkspaceApi>,
Query(query): Query<ObjectiveListQuery>,
) -> ApiResult<Json<ListResponse<crate::records::ObjectiveSummary>>> {
let limit = query.limit.unwrap_or(api.config.max_records).min(1000);
let ProjectRecordList {
items,
invalid_records,
record_authority,
} = api.authority.list_objectives(limit)?;
Ok(Json(ListResponse {
workspace_id: api.config.workspace_id,
limit,
items,
invalid_records,
record_authority,
}))
}
async fn get_objective(
State(api): State<WorkspaceApi>,
AxumPath(id): AxumPath<String>,
) -> ApiResult<Json<ObjectiveDetail>> {
Ok(Json(api.authority.objective(&id)?))
}
async fn list_repositories(
State(api): State<WorkspaceApi>,
) -> ApiResult<Json<RepositoryListResponse>> {
let RepositoryListProjection { items, diagnostics } = api.repository_reader().list();
Ok(Json(RepositoryListResponse {
workspace_id: api.config.workspace_id,
items,
source: "workspace-control-plane".to_string(),
diagnostics: repository_diagnostics(diagnostics),
}))
}
async fn repository_detail(
State(api): State<WorkspaceApi>,
AxumPath(repository_id): AxumPath<String>,
) -> ApiResult<Json<RepositoryDetailResponse>> {
let item = repository_lookup(api.repository_reader().summary(&repository_id))?;
Ok(Json(RepositoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
item,
source: "workspace-control-plane".to_string(),
}))
}
async fn repository_log(
State(api): State<WorkspaceApi>,
AxumPath(repository_id): AxumPath<String>,
Query(query): Query<LogQuery>,
) -> ApiResult<Json<RepositoryLogResponse>> {
let RepositoryLogRead {
repository_id,
default_selector,
limit,
commits,
diagnostics,
} = repository_lookup(
api.repository_reader()
.recent_log(&repository_id, query.limit),
)?;
Ok(Json(RepositoryLogResponse {
workspace_id: api.config.workspace_id,
repository_id,
default_selector,
limit,
items: commits,
diagnostics: repository_diagnostics(diagnostics),
}))
}
async fn list_hosts(
State(api): State<WorkspaceApi>,
) -> ApiResult<Json<RuntimeListResponse<HostSummary>>> {
let limit = api.config.max_records.min(200);
let runtime_hosts = api.runtime.list_hosts(limit);
Ok(Json(RuntimeListResponse {
workspace_id: api.config.workspace_id,
limit,
items: runtime_hosts.items,
source: "worker_runtime_registry".to_string(),
diagnostics: runtime_hosts.diagnostics,
}))
}
async fn list_runtimes(
State(api): State<WorkspaceApi>,
) -> ApiResult<Json<RuntimeListResponse<RuntimeSummary>>> {
let limit = api.config.max_records.min(200);
let runtimes = api.runtime.list_runtimes(limit);
Ok(Json(RuntimeListResponse {
workspace_id: api.config.workspace_id,
limit,
items: runtimes.items,
source: "worker_runtime_registry".to_string(),
diagnostics: runtimes.diagnostics,
}))
}
async fn list_workers(
State(api): State<WorkspaceApi>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
workers_response(api).map(Json)
}
async fn get_runtime_connection_settings(
State(api): State<WorkspaceApi>,
) -> ApiResult<Json<RuntimeConnectionSettingsResponse>> {
let runtime_config = load_backend_runtimes_config_for_settings(&api)?;
Ok(Json(runtime_connection_settings_response(
&api,
&runtime_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 runtime_config = load_backend_runtimes_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 runtime_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",
));
}
let remote_config = 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,
};
let active_config = remote_runtime_config_from_file(&remote_config).map_err(|diagnostic| {
ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: remote_config.id.clone(),
code: diagnostic.code.clone(),
message: diagnostic.message.clone(),
},
vec![diagnostic],
)
})?;
let active_runtime = RemoteWorkerRuntime::new(
active_config,
api.config.workspace_id.clone(),
api.config
.backend_base_url
.clone()
.unwrap_or_else(|| "http://127.0.0.1:8787".to_string()),
)
.map(|host| host.with_resource_broker(api.resource_broker.clone()))
.map_err(|err| err.into_error())?;
runtime_config.runtimes.remote.push(remote_config);
write_backend_runtimes_config_for_settings(&api, &runtime_config)?;
api.runtime.register_or_replace(active_runtime);
let mut response = runtime_connection_mutation_response(
&api,
&runtime_config,
vec![settings_diagnostic(
"runtime_registry_applied",
DiagnosticSeverity::Info,
"Remote Runtime config was persisted and applied to the active Runtime registry without restarting the Workspace backend.",
)],
);
response.diagnostics.push(settings_diagnostic(
"backend_runtimes_config_rewritten",
DiagnosticSeverity::Info,
"Backend runtimes 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 runtime_config = load_backend_runtimes_config_for_settings(&api)?;
let before = runtime_config.runtimes.remote.len();
runtime_config
.runtimes
.remote
.retain(|remote| remote.id != runtime_id);
if before == runtime_config.runtimes.remote.len() {
return Err(Error::UnknownRuntime(runtime_id).into());
}
match api
.runtime
.unregister_if_idle(&runtime_id, api.config.max_records.min(200))
.map_err(|err| err.into_error())?
{
RuntimeRegistryUnregisterResult::Removed | RuntimeRegistryUnregisterResult::NotFound => {}
RuntimeRegistryUnregisterResult::BlockedByWorkers {
worker_count,
diagnostics,
} => {
let mut diagnostics = diagnostics;
diagnostics.push(settings_diagnostic(
"remote_runtime_delete_blocked",
DiagnosticSeverity::Error,
format!(
"Remote Runtime '{runtime_id}' has {worker_count} active worker(s); stop or move them before deleting the connection."
),
));
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id,
code: "remote_runtime_delete_blocked".to_string(),
message: "Remote Runtime connection has active workers".to_string(),
},
diagnostics,
));
}
}
write_backend_runtimes_config_for_settings(&api, &runtime_config)?;
let mut response = runtime_connection_mutation_response(
&api,
&runtime_config,
vec![settings_diagnostic(
"runtime_registry_applied",
DiagnosticSeverity::Info,
"Remote Runtime config was removed from persisted config and the active Runtime registry without restarting the Workspace backend.",
)],
);
response.diagnostics.push(settings_diagnostic(
"backend_runtimes_config_rewritten",
DiagnosticSeverity::Info,
"Backend runtimes 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 runtime_config = load_backend_runtimes_config_for_settings(&api)?;
let remote = runtime_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)))
}
fn working_directory_request_from_repository(
repository: &ConfiguredRepository,
selector: Option<&str>,
) -> WorkingDirectoryRequest {
WorkingDirectoryRequest {
repository: WorkingDirectoryRepository {
id: repository.id.clone(),
provider: repository.provider.clone(),
uri: repository.uri.clone(),
local_path: Some(repository.path.clone()),
selector: selector
.map(|selector| RuntimeRepositorySelector::from(selector.to_string()))
.or_else(|| {
repository
.default_selector
.clone()
.map(RuntimeRepositorySelector)
})
.or_else(|| Some(RuntimeRepositorySelector::from("HEAD"))),
},
materializer: MaterializerKind::LocalGitWorktree,
backend_workdir_id: None,
}
}
fn configured_working_directory_request(
config: &ServerConfig,
request: &WorkerSpawnWorkingDirectoryRequest,
) -> Result<WorkingDirectoryRequest> {
let repository = config
.repositories
.iter()
.find(|repository| repository.id == request.repository_id)
.ok_or_else(|| {
Error::Config(format!(
"unknown repository id `{}` for Worker working directory",
request.repository_id
))
})?;
Ok(working_directory_request_from_repository(
repository,
request.selector.as_deref(),
))
}
async fn create_workspace_worker(
State(api): State<WorkspaceApi>,
Json(request): Json<BrowserCreateWorkerRequest>,
) -> ApiResult<Json<BrowserCreateWorkerResponse>> {
let profile = request
.profile
.as_deref()
.map(str::trim)
.filter(|profile| !profile.is_empty())
.map(ToOwned::to_owned)
.or_else(|| {
crate::profile_settings::load_profile_settings(
&api.config.workspace_id,
&api.config.workspace_root,
)
.default_profile
})
.ok_or_else(|| {
settings_bad_request(
"workspace_default_profile_missing",
"profile is required because this Workspace has no default profile configured",
)
})?;
let profile_selector =
profile_selector_for_candidate_with_root(&api.config.workspace_root, &profile).ok_or_else(
|| {
settings_bad_request(
"unsupported_worker_profile",
"profile must be selected from Backend-published worker profile candidates",
)
},
)?;
let resolved_config_bundle = if profile.starts_with("project:") {
crate::profile_settings::build_workspace_profile_config_bundle(
&api.config.workspace_root,
&api.config.workspace_id,
&api.config.workspace_created_at,
&profile,
)?
} else {
None
};
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",
)
})?;
if display_name == crate::hosts::WORKSPACE_ORCHESTRATOR_SINGLETON_KEY {
return Err(Error::ReservedWorkerName(display_name).into());
}
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,
segments: None,
})
};
let selected_working_directory_id = request
.working_directory
.as_ref()
.map(|selection| selection.working_directory_id.clone());
let resolved_working_directory =
request
.working_directory
.map(|selection| WorkingDirectoryClaim {
working_directory_id: selection.working_directory_id,
relative_cwd: selection.relative_cwd,
});
validate_working_directory_claim_for_browser(resolved_working_directory.as_ref())?;
if resolved_working_directory.is_none() {
reject_no_workdir_for_non_embedded_runtime(&request.runtime_id)?;
}
let result = api.spawn_workspace_worker(
&request.runtime_id,
WorkerSpawnRequest {
requested_worker_name: Some(display_name.clone()),
intent: WorkerSpawnIntent::WorkspaceCoding,
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: if initial_input.is_some() { 1 } else { 0 },
},
profile: profile_selector,
ticket_assignment: None,
initial_input,
working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory,
resolved_config_bundle,
resolved_workspace_api: None,
},
)?;
Ok(Json(record_browser_worker_spawn(
&api,
request.runtime_id,
display_name,
selected_working_directory_id,
result,
)?))
}
fn record_browser_worker_spawn(
api: &WorkspaceApi,
requested_runtime_id: String,
display_name: String,
selected_working_directory_id: Option<String>,
result: WorkerSpawnResult,
) -> ApiResult<BrowserCreateWorkerResponse> {
if result.state != WorkerOperationState::Accepted {
return Err(worker_create_not_accepted_error(
requested_runtime_id.clone(),
result.diagnostics,
));
}
let worker = result.worker.ok_or_else(|| Error::RuntimeOperationFailed {
runtime_id: requested_runtime_id,
code: "workspace_worker_create_missing_summary".to_string(),
message: "Runtime completed worker creation without returning a Worker summary".to_string(),
})?;
let worker_record = record_worker_summary(
api,
&worker,
display_name.as_str(),
worker.profile.clone(),
WorkerRegistryDisplayNamePolicy::UseProvided,
)?;
if let Some(working_directory) = worker.working_directory.as_ref() {
let workdir_record =
workdir_record_from_summary(api, worker.runtime_id.as_str(), working_directory);
api.store.upsert_workdir_registry(&workdir_record)?;
link_worker_to_workdir(api, &worker_record, &working_directory.working_directory_id)?;
}
if let Some(workdir_id) = selected_working_directory_id.as_deref() {
if api
.store
.get_workdir_registry(&api.config.workspace_id, workdir_id)?
.is_none()
{
if let Ok(result) = api
.runtime
.working_directory(worker.runtime_id.as_str(), workdir_id)
.map_err(|err| err.into_error())
{
if let Some(status) = result.working_directory {
let record = workdir_record_from_summary(
api,
worker.runtime_id.as_str(),
&status.summary,
);
api.store.upsert_workdir_registry(&record)?;
}
}
}
if api
.store
.get_workdir_registry(&api.config.workspace_id, workdir_id)?
.is_some()
{
link_worker_to_workdir(api, &worker_record, workdir_id)?;
}
}
let runtime_id = worker.runtime_id.clone();
let worker_id = worker.worker_id.clone();
let workspace_id = api.workspace_id().to_string();
let console_href = format!(
"/w/{}/runtimes/{}/workers/{}/console",
encode_path_segment(&workspace_id),
encode_path_segment(&runtime_id),
encode_path_segment(&worker_id)
);
Ok(BrowserCreateWorkerResponse {
workspace_id,
runtime_id,
worker_id,
console_href,
worker,
diagnostics: result.diagnostics,
})
}
async fn post_internal_runtime_resource_fetch(
State(api): State<WorkspaceApi>,
Json(request): Json<BackendResourceFetchRequest>,
) -> std::result::Result<
Json<worker_runtime::resource::BackendResourceFetchResponse>,
(StatusCode, Json<BackendResourceError>),
> {
api.resource_broker
.fetch_profile_source_archive(request)
.map(Json)
.map_err(|error| (backend_resource_error_status(&error), Json(error)))
}
fn backend_resource_error_status(error: &BackendResourceError) -> StatusCode {
match error {
BackendResourceError::Expired => StatusCode::GONE,
BackendResourceError::Unauthorized { .. } => StatusCode::UNAUTHORIZED,
BackendResourceError::MissingResource => StatusCode::NOT_FOUND,
BackendResourceError::UnsupportedKind
| BackendResourceError::DigestMismatch { .. }
| BackendResourceError::Oversized { .. }
| BackendResourceError::ContentTypeMismatch { .. }
| BackendResourceError::InvalidResponse { .. } => StatusCode::BAD_REQUEST,
BackendResourceError::Transport { .. } => StatusCode::BAD_GATEWAY,
}
}
async fn get_companion_status(
State(api): State<WorkspaceApi>,
) -> ApiResult<Json<CompanionStatusResponse>> {
Ok(Json(api.companion.status()))
}
async fn get_companion_transcript(
State(api): State<WorkspaceApi>,
Query(query): Query<TranscriptQuery>,
) -> ApiResult<Json<CompanionTranscriptProjection>> {
let limit = query.limit.unwrap_or(api.config.max_records).min(200);
let start = query.start.unwrap_or(0);
Ok(Json(api.companion.transcript(start, limit)))
}
async fn post_companion_message(
State(api): State<WorkspaceApi>,
Json(request): Json<CompanionMessageRequest>,
) -> ApiResult<Json<CompanionMessageResponse>> {
Ok(Json(api.companion.send_message(request)))
}
async fn post_companion_cancel(
State(api): State<WorkspaceApi>,
Json(request): Json<CompanionCancelRequest>,
) -> ApiResult<Json<CompanionMessageResponse>> {
Ok(Json(api.companion.cancel(request)))
}
async fn get_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>,
) -> ApiResult<Json<WorkerSummary>> {
let worker = api
.runtime
.worker(&runtime_id, &worker_id)
.map_err(|err| err.into_error())?;
let record = sync_worker_observation(&api, &worker)?;
let links = api.store.list_worker_workdir_links(
&api.config.workspace_id,
record.runtime_id.as_str(),
record.runtime_worker_id,
)?;
let workdirs = api
.store
.list_workdir_registry(&api.config.workspace_id, 500)?;
Ok(Json(merge_worker_registry_projection(
Some(&worker),
&record,
links,
&workdirs,
)))
}
async fn restore_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>,
) -> ApiResult<Json<WorkerRestoreResponse>> {
let mut result = api.restore_workspace_worker(&runtime_id, &worker_id)?;
if let Some(worker) = result.worker.as_ref() {
let record = sync_worker_observation(&api, worker)?;
let links = api.store.list_worker_workdir_links(
&api.config.workspace_id,
record.runtime_id.as_str(),
record.runtime_worker_id,
)?;
let workdirs = api
.store
.list_workdir_registry(&api.config.workspace_id, 500)?;
result.worker = Some(merge_worker_registry_projection(
Some(worker),
&record,
links,
&workdirs,
));
}
Ok(Json(WorkerRestoreResponse {
workspace_id: api.workspace_id().to_string(),
runtime_id,
worker_id,
result,
}))
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RuntimeConfigBundleSyncRequest {
pub bundle: ConfigBundle,
}
#[derive(Debug, Deserialize)]
struct RuntimeConfigBundleAvailabilityQuery {
digest: String,
}
fn reject_workdir_for_embedded_runtime(
runtime_id: &str,
has_workdir: bool,
) -> std::result::Result<(), ApiError> {
if runtime_id != EMBEDDED_WORKER_RUNTIME_ID || !has_workdir {
return Ok(());
}
Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: runtime_id.to_string(),
code: "embedded_worker_workdir_unsupported".to_string(),
message: "The embedded Runtime does not accept working directories".to_string(),
},
vec![RuntimeDiagnostic {
code: "embedded_worker_workdir_unsupported".to_string(),
severity: DiagnosticSeverity::Error,
message: "Choose a non-embedded Runtime for workspace-file Workers; embedded Workers are no-workdir Workspace-API workers.".to_string(),
}],
))
}
fn reject_no_workdir_for_non_embedded_runtime(
runtime_id: &str,
) -> std::result::Result<(), ApiError> {
if runtime_id == EMBEDDED_WORKER_RUNTIME_ID {
return Ok(());
}
Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: runtime_id.to_string(),
code: "workspace_worker_workdir_required".to_string(),
message: "Only the embedded Runtime can launch a Worker without a working directory"
.to_string(),
},
vec![RuntimeDiagnostic {
code: "workspace_worker_workdir_required".to_string(),
severity: DiagnosticSeverity::Error,
message: "Select a working directory for this Runtime, or choose the embedded Runtime for a conversation-only Worker."
.to_string(),
}],
))
}
async fn list_runtime_workers(
State(api): State<WorkspaceApi>,
AxumPath(runtime_id): AxumPath<String>,
Query(query): Query<RuntimeWorkersQuery>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
let limit = api.config.max_records.min(200);
let (worker_list, source) = match query.status {
Some(RuntimeWorkersStatusFilter::Stopped) => (
api.runtime
.list_stopped_workers_for_runtime(&runtime_id, limit)
.map_err(|err| err.into_error())?,
"runtime_registry_stopped",
),
None => (
api.runtime
.list_workers_for_runtime(&runtime_id, limit)
.map_err(|err| err.into_error())?,
"runtime_registry",
),
};
Ok(Json(RuntimeListResponse {
workspace_id: api.workspace_id().to_string(),
limit,
items: worker_list.items,
source: source.to_string(),
diagnostics: worker_list.diagnostics,
}))
}
async fn create_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath(runtime_id): AxumPath<String>,
Json(mut request): Json<WorkerSpawnRequest>,
) -> ApiResult<Json<WorkerSpawnResult>> {
if let Some(assignment) = request.ticket_assignment.as_ref() {
if let Some(worker) = existing_lifecycle_assignment_worker(&api, assignment, &runtime_id)? {
assign_ticket_worker_from_lifecycle(&api, assignment, &runtime_id, &worker.worker_id)?;
return Ok(Json(WorkerSpawnResult {
state: WorkerOperationState::Accepted,
worker: Some(worker),
acceptance_evidence: Vec::new(),
diagnostics: Vec::new(),
}));
}
}
let lifecycle_assignment = request.ticket_assignment.clone();
reject_workdir_for_embedded_runtime(
&runtime_id,
request.working_directory_request.is_some() || request.resolved_working_directory.is_some(),
)?;
if request.working_directory_request.is_none() && request.resolved_working_directory.is_none() {
reject_no_workdir_for_non_embedded_runtime(&runtime_id)?;
}
request.resolved_working_directory_request = request
.working_directory_request
.as_ref()
.map(|working_directory| {
configured_working_directory_request(&api.config, working_directory)
})
.transpose()?;
let prepared_workdir_id = if let Some(working_directory_request) =
request.resolved_working_directory_request.as_mut()
{
Some(upsert_pending_backend_workdir(
&api,
&runtime_id,
working_directory_request,
)?)
} else {
request
.resolved_working_directory
.as_ref()
.map(|claim| claim.working_directory_id.clone())
};
let requested_worker_name = request.requested_worker_name.clone();
let spawn_idempotency =
crate::hosts::worker_spawn_idempotency(&request).map_err(Error::Config)?;
if let (Some(assignment), Some((_, fingerprint))) =
(lifecycle_assignment.as_ref(), spawn_idempotency.as_ref())
{
api.store.reserve_ticket_assignment_operation(
&api.config.workspace_id,
&assignment.operation_id,
&assignment.ticket_id,
&runtime_id,
None,
fingerprint,
&Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
)?;
}
let result = api.spawn_workspace_worker(&runtime_id, request)?;
if let Some(worker) = result.worker.as_ref() {
let display_name = requested_worker_name
.as_deref()
.filter(|name| !name.trim().is_empty())
.unwrap_or(worker.label.as_str())
.to_string();
let record = record_worker_summary(
&api,
worker,
display_name.as_str(),
worker.profile.clone(),
WorkerRegistryDisplayNamePolicy::UseProvided,
)?;
if let Some(assignment) = lifecycle_assignment.as_ref() {
api.store.bind_ticket_assignment_operation_worker(
&api.config.workspace_id,
&assignment.operation_id,
&worker.worker_id,
)?;
assign_ticket_worker_from_lifecycle(&api, assignment, &runtime_id, &worker.worker_id)?;
}
if worker.working_directory.is_none() {
if let Some(workdir_id) = prepared_workdir_id.as_deref() {
if api
.store
.get_workdir_registry(&api.config.workspace_id, workdir_id)?
.is_some()
{
link_worker_to_workdir(&api, &record, workdir_id)?;
}
}
}
} else if let Some(workdir_id) = prepared_workdir_id.as_deref() {
if let Some(mut record) = api
.store
.get_workdir_registry(&api.config.workspace_id, workdir_id)?
{
record.materialization_status = "failed".to_string();
record.updated_at = now_registry_timestamp();
api.store.upsert_workdir_registry(&record)?;
}
}
Ok(Json(result))
}
async fn sync_runtime_config_bundle(
State(api): State<WorkspaceApi>,
AxumPath(runtime_id): AxumPath<String>,
Json(request): Json<RuntimeConfigBundleSyncRequest>,
) -> ApiResult<Json<ConfigBundleSyncResult>> {
let result = api
.runtime
.sync_config_bundle(&runtime_id, request.bundle)
.map_err(|err| err.into_error())?;
Ok(Json(result))
}
async fn check_runtime_config_bundle(
State(api): State<WorkspaceApi>,
AxumPath((runtime_id, bundle_id)): AxumPath<(String, String)>,
Query(query): Query<RuntimeConfigBundleAvailabilityQuery>,
) -> ApiResult<Json<ConfigBundleCheckResult>> {
let result = api
.runtime
.check_config_bundle(
&runtime_id,
ConfigBundleRef {
id: bundle_id,
digest: query.digest,
},
)
.map_err(|err| err.into_error())?;
Ok(Json(result))
}
async fn send_runtime_worker_input(
State(api): State<WorkspaceApi>,
AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>,
Json(request): Json<WorkerInputRequest>,
) -> ApiResult<Json<WorkerInputResult>> {
let result = api
.runtime
.send_input(&runtime_id, &worker_id, request)
.map_err(|err| err.into_error())?;
Ok(Json(result))
}
async fn runtime_worker_completions(
State(api): State<WorkspaceApi>,
AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>,
Json(request): Json<WorkerCompletionsRequest>,
) -> ApiResult<Json<WorkerCompletionsResult>> {
let result = api
.runtime
.worker_completions(&runtime_id, &worker_id, request)
.map_err(|err| err.into_error())?;
Ok(Json(result))
}
async fn stop_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>,
Json(request): Json<WorkerLifecycleRequest>,
) -> ApiResult<Json<WorkerLifecycleResult>> {
let result = api
.runtime
.stop_worker(&runtime_id, &worker_id, request)
.map_err(|err| err.into_error())?;
let runtime_worker_id = parse_runtime_worker_id_for_registry(&worker_id)?;
if let Some(record) =
api.store
.get_worker_registry(&api.config.workspace_id, &runtime_id, runtime_worker_id)?
{
sync_linked_workdir_after_worker_stop(&api, &runtime_id, &record)?;
}
Ok(Json(result))
}
async fn cancel_runtime_worker(
State(api): State<WorkspaceApi>,
AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>,
Json(request): Json<WorkerLifecycleRequest>,
) -> ApiResult<Json<WorkerLifecycleResult>> {
let result = api
.runtime
.cancel_worker(&runtime_id, &worker_id, request)
.map_err(|err| err.into_error())?;
Ok(Json(result))
}
async fn worker_protocol_ws(
State(api): State<WorkspaceApi>,
AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>,
ws: WebSocketUpgrade,
) -> impl IntoResponse {
let source = match api.observation_proxy.source(&runtime_id, &worker_id) {
Ok(source) => source,
Err(ObservationProxyError::WorkerNotFound(_)) => {
match api.runtime.observation_source(&runtime_id, &worker_id) {
Ok(source) => source,
Err(error) => return ApiError::from(error.into_error()).into_response(),
}
}
Err(error) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": error.code(),
"message": error.message(),
})),
)
.into_response();
}
};
ws.on_upgrade(move |socket| worker_protocol_ws_session(source, socket))
}
pub(crate) struct WorkspaceWorkerProtocolConnection {
pub(crate) methods: tokio::sync::mpsc::Sender<protocol::Method>,
pub(crate) events: tokio::sync::mpsc::Receiver<protocol::Event>,
}
pub(crate) async fn connect_workspace_worker_protocol(
api: &WorkspaceApi,
runtime_id: &str,
worker_id: &str,
) -> Result<WorkspaceWorkerProtocolConnection> {
let source = match api.observation_proxy.source(runtime_id, worker_id) {
Ok(source) => source,
Err(ObservationProxyError::WorkerNotFound(_)) => api
.runtime
.observation_source(runtime_id, worker_id)
.map_err(|error| error.into_error())?,
Err(error) => {
return Err(Error::RuntimeOperationFailed {
runtime_id: runtime_id.to_string(),
code: error.code().to_string(),
message: error.message().to_string(),
});
}
};
match source {
RuntimeObservationSource::RemoteWs(config) => connect_remote_worker_protocol(config).await,
RuntimeObservationSource::Embedded(source) => {
connect_embedded_worker_protocol(source).await
}
}
}
async fn connect_remote_worker_protocol(
config: RuntimeObservationSourceConfig,
) -> Result<WorkspaceWorkerProtocolConnection> {
let mut request = config
.endpoint
.clone()
.into_client_request()
.map_err(|error| Error::Config(format!("invalid Runtime protocol endpoint: {error}")))?;
if let Some(token) = &config.bearer_token {
request.headers_mut().insert(
"authorization",
format!("Bearer {token}").parse().map_err(|error| {
Error::Config(format!("invalid Runtime authorization: {error}"))
})?,
);
}
let (socket, _) =
connect_async(request)
.await
.map_err(|error| Error::RuntimeOperationFailed {
runtime_id: config.runtime_id.clone(),
code: "worker_protocol_connect_failed".to_string(),
message: error.to_string(),
})?;
let (mut sink, mut stream) = socket.split();
let (methods, mut method_receiver) = tokio::sync::mpsc::channel(256);
let (event_sender, events) = tokio::sync::mpsc::channel(512);
tokio::spawn(async move {
loop {
tokio::select! {
method = method_receiver.recv() => {
let Some(method) = method else { break };
let Ok(text) = protocol::stream::encode_method(&method) else { break };
if sink.send(TungsteniteMessage::Text(text.into())).await.is_err() { break; }
}
message = stream.next() => match message {
Some(Ok(TungsteniteMessage::Text(text))) => {
let Ok(event) = protocol::stream::decode_event(text.as_ref()) else { break; };
if event_sender.send(event).await.is_err() { break; }
}
Some(Ok(TungsteniteMessage::Ping(value))) => {
if sink.send(TungsteniteMessage::Pong(value)).await.is_err() { break; }
}
Some(Ok(TungsteniteMessage::Pong(_))) => {}
_ => break,
}
}
}
});
Ok(WorkspaceWorkerProtocolConnection { methods, events })
}
async fn connect_embedded_worker_protocol(
source: crate::observation::EmbeddedRuntimeObservationSource,
) -> Result<WorkspaceWorkerProtocolConnection> {
let mut upstream =
RuntimeObservationClient::connect(&RuntimeObservationSource::Embedded(source.clone()))
.await
.map_err(|error| Error::RuntimeOperationFailed {
runtime_id: source.runtime_id.clone(),
code: error.code().to_string(),
message: error.message().to_string(),
})?;
let (methods, mut method_receiver) = tokio::sync::mpsc::channel(256);
let (event_sender, events) = tokio::sync::mpsc::channel(512);
tokio::spawn(async move {
loop {
tokio::select! {
method = method_receiver.recv() => {
let Some(method) = method else { break };
match source.runtime.send_protocol_method(&source.worker_ref, method) {
Ok(direct_events) => {
for event in direct_events {
if event_sender.send(event).await.is_err() { return; }
}
}
Err(error) => {
if event_sender.send(protocol_error_event(error.to_string())).await.is_err() { return; }
}
}
}
event = upstream.next_event() => match event {
Ok(event) => {
if event_sender.send(event.payload).await.is_err() { break; }
}
Err(_) => break,
}
}
}
});
Ok(WorkspaceWorkerProtocolConnection { methods, events })
}
async fn worker_protocol_ws_session(source: RuntimeObservationSource, socket: WebSocket) {
match source {
RuntimeObservationSource::RemoteWs(config) => {
remote_worker_protocol_ws_session(config, socket).await;
}
RuntimeObservationSource::Embedded(source) => {
embedded_worker_protocol_ws_session(source, socket).await;
}
}
}
async fn remote_worker_protocol_ws_session(
config: RuntimeObservationSourceConfig,
socket: WebSocket,
) {
let mut request = match config.endpoint.clone().into_client_request() {
Ok(request) => request,
Err(error) => {
let mut socket = socket;
let event = protocol_error_event(format!(
"failed to build runtime protocol WebSocket request: {error}"
));
let _ = send_protocol_event(&mut socket, &event).await;
return;
}
};
if let Some(token) = &config.bearer_token {
match format!("Bearer {token}").parse() {
Ok(value) => {
request.headers_mut().insert("authorization", value);
}
Err(error) => {
let mut socket = socket;
let event = protocol_error_event(format!(
"failed to build runtime authorization header: {error}"
));
let _ = send_protocol_event(&mut socket, &event).await;
return;
}
}
}
let (upstream, _) = match connect_async(request).await {
Ok(connection) => connection,
Err(error) => {
let mut socket = socket;
let event = protocol_error_event(format!(
"failed to connect runtime protocol WebSocket: {error}"
));
let _ = send_protocol_event(&mut socket, &event).await;
return;
}
};
let (mut client_sink, mut client_stream) = socket.split();
let (mut upstream_sink, mut upstream_stream) = upstream.split();
loop {
tokio::select! {
inbound = client_stream.next() => {
match inbound {
Some(Ok(WsMessage::Text(text))) => {
if upstream_sink.send(TungsteniteMessage::Text(text.to_string().into())).await.is_err() {
break;
}
}
Some(Ok(WsMessage::Binary(binary))) => {
if upstream_sink.send(TungsteniteMessage::Binary(binary.to_vec().into())).await.is_err() {
break;
}
}
Some(Ok(WsMessage::Close(_))) | None => {
let _ = upstream_sink.send(TungsteniteMessage::Close(None)).await;
break;
}
Some(Ok(WsMessage::Ping(payload))) => {
if upstream_sink.send(TungsteniteMessage::Ping(payload.to_vec().into())).await.is_err() {
break;
}
}
Some(Ok(WsMessage::Pong(payload))) => {
if upstream_sink.send(TungsteniteMessage::Pong(payload.to_vec().into())).await.is_err() {
break;
}
}
Some(Err(_)) => break,
}
}
outbound = upstream_stream.next() => {
match outbound {
Some(Ok(TungsteniteMessage::Text(text))) => {
if client_sink.send(WsMessage::Text(text.to_string().into())).await.is_err() {
break;
}
}
Some(Ok(TungsteniteMessage::Binary(binary))) => {
if client_sink.send(WsMessage::Binary(binary.to_vec().into())).await.is_err() {
break;
}
}
Some(Ok(TungsteniteMessage::Close(_))) | None => {
let _ = client_sink.send(WsMessage::Close(None)).await;
break;
}
Some(Ok(TungsteniteMessage::Ping(payload))) => {
if client_sink.send(WsMessage::Ping(payload.to_vec().into())).await.is_err() {
break;
}
}
Some(Ok(TungsteniteMessage::Pong(payload))) => {
if client_sink.send(WsMessage::Pong(payload.to_vec().into())).await.is_err() {
break;
}
}
Some(Ok(TungsteniteMessage::Frame(_))) => {}
Some(Err(_)) => break,
}
}
}
}
}
async fn embedded_worker_protocol_ws_session(
source: crate::observation::EmbeddedRuntimeObservationSource,
mut socket: WebSocket,
) {
let mut upstream = match RuntimeObservationClient::connect(&RuntimeObservationSource::Embedded(
source.clone(),
))
.await
{
Ok(client) => client,
Err(error) => {
let event = protocol_error_event(error.message());
let _ = send_protocol_event(&mut socket, &event).await;
return;
}
};
loop {
tokio::select! {
inbound = socket.next() => {
match inbound {
Some(Ok(WsMessage::Text(text))) => match decode_method(&text) {
Ok(method) => match source.runtime.send_protocol_method(&source.worker_ref, method) {
Ok(events) => {
for event in events {
if !send_protocol_event(&mut socket, &event).await {
return;
}
}
}
Err(error) => {
let event = protocol_error_event(error.to_string());
if !send_protocol_event(&mut socket, &event).await {
return;
}
}
},
Err(error) => {
let event =
protocol_error_event(format!("malformed protocol method frame: {error}"));
if !send_protocol_event(&mut socket, &event).await {
return;
}
}
},
Some(Ok(WsMessage::Close(_))) | None => return,
Some(Ok(WsMessage::Ping(payload))) => {
if socket.send(WsMessage::Pong(payload)).await.is_err() {
return;
}
}
Some(Ok(WsMessage::Pong(_))) | Some(Ok(WsMessage::Binary(_))) => {}
Some(Err(error)) => {
let event = protocol_error_event(format!("protocol WebSocket error: {error}"));
let _ = send_protocol_event(&mut socket, &event).await;
return;
}
}
}
upstream_event = upstream.next_event() => {
match upstream_event {
Ok(event) => {
if !send_protocol_event(&mut socket, &event.payload).await {
return;
}
}
Err(error) => {
let event = protocol_error_event(error.message());
let _ = send_protocol_event(&mut socket, &event).await;
return;
}
}
}
}
}
}
async fn send_protocol_event(socket: &mut WebSocket, event: &protocol::Event) -> bool {
match encode_event(event) {
Ok(text) => socket.send(WsMessage::Text(text.into())).await.is_ok(),
Err(error) => {
let fallback = protocol_error_event(format!(
"failed to serialize protocol response event: {error}"
));
let Ok(text) = encode_event(&fallback) else {
return false;
};
socket.send(WsMessage::Text(text.into())).await.is_ok()
}
}
}
fn protocol_error_event(message: impl Into<String>) -> protocol::Event {
protocol::Event::Error {
code: protocol::ErrorCode::Internal,
message: message.into(),
}
}
async fn list_host_workers(
State(api): State<WorkspaceApi>,
AxumPath(host_id): AxumPath<String>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
let limit = api.config.max_records.min(200);
let runtime_workers = api
.runtime
.list_workers_for_host(&host_id, limit)
.map_err(|err| err.into_error())?;
Ok(Json(RuntimeListResponse {
workspace_id: api.config.workspace_id,
limit,
items: runtime_workers.items,
source: "worker_runtime_registry".to_string(),
diagnostics: runtime_workers.diagnostics,
}))
}
fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSummary>> {
let limit = api.config.max_records.min(200);
let runtime_workers = api.runtime.list_workers(limit);
let mut observed = std::collections::BTreeMap::new();
for worker in &runtime_workers.items {
let _ = sync_worker_observation(&api, worker);
if let Ok(runtime_worker_id) =
parse_runtime_worker_id_for_registry(worker.worker_id.as_str())
{
observed.insert(
(worker.runtime_id.clone(), runtime_worker_id),
worker.clone(),
);
}
}
let mut diagnostics = runtime_workers.diagnostics;
let worker_records = api
.store
.list_worker_registry(&api.config.workspace_id, limit)?;
let workdir_records = api
.store
.list_workdir_registry(&api.config.workspace_id, 500)?;
let mut items = Vec::new();
for record in worker_records {
if !observed.contains_key(&(record.runtime_id.clone(), record.runtime_worker_id)) {
match api.runtime.worker(
record.runtime_id.as_str(),
&record.runtime_worker_id.to_string(),
) {
Ok(worker) => {
let _ = sync_worker_observation(&api, &worker);
observed.insert(
(worker.runtime_id.clone(), record.runtime_worker_id),
worker,
);
}
Err(RuntimeRegistryError::UnknownWorker { .. }) => {}
Err(error) => diagnostics.push(RuntimeDiagnostic {
code: "worker_detail_probe_failed".to_string(),
severity: DiagnosticSeverity::Info,
message: format!(
"Could not verify Worker {} on Runtime {}: {}",
record.runtime_worker_id,
record.runtime_id,
sanitize_backend_error(&error.into_error().to_string())
),
}),
}
}
let links = api.store.list_worker_workdir_links(
&api.config.workspace_id,
record.runtime_id.as_str(),
record.runtime_worker_id,
)?;
items.push(merge_worker_registry_projection(
observed.get(&(record.runtime_id.clone(), record.runtime_worker_id)),
&record,
links,
&workdir_records,
));
}
Ok(RuntimeListResponse {
workspace_id: api.config.workspace_id,
limit,
items,
source: "backend_worker_registry".to_string(),
diagnostics,
})
}
fn load_backend_runtimes_config_for_settings(
api: &WorkspaceApi,
) -> ApiResult<BackendRuntimesConfigFile> {
api.config
.runtime_config_path
.as_ref()
.map(BackendRuntimesConfigFile::load_from_path)
.transpose()
.map_err(|error| {
Error::Config(format!(
"failed to read Backend runtimes config for Runtime connections: {}",
sanitize_backend_error(&error.to_string())
))
.into()
})
.map(|config| config.unwrap_or_default())
}
fn write_backend_runtimes_config_for_settings(
api: &WorkspaceApi,
runtime_config: &BackendRuntimesConfigFile,
) -> ApiResult<()> {
let path = api.config.runtime_config_path.as_ref().ok_or_else(|| {
Error::Config(
"Backend runtimes config path is unavailable; set YOI_CONFIG_DIR, YOI_HOME, XDG_CONFIG_HOME, or HOME"
.to_string(),
)
})?;
runtime_config.write_to_path(path).map_err(|error| {
Error::Config(format!(
"failed to write Backend runtimes config for Runtime connections: {}",
sanitize_backend_error(&error.to_string())
))
.into()
})
}
fn runtime_connection_settings_response(
api: &WorkspaceApi,
runtime_config: &BackendRuntimesConfigFile,
) -> RuntimeConnectionSettingsResponse {
RuntimeConnectionSettingsResponse {
workspace_id: api.config.workspace_id.clone(),
embedded: embedded_runtime_connection_summary(api),
remotes: remote_runtime_connection_summaries(api, runtime_config, false),
diagnostics: Vec::new(),
}
}
fn runtime_connection_mutation_response(
api: &WorkspaceApi,
runtime_config: &BackendRuntimesConfigFile,
diagnostics: Vec<RuntimeDiagnostic>,
) -> RuntimeConnectionMutationResponse {
RuntimeConnectionMutationResponse {
workspace_id: api.config.workspace_id.clone(),
restart_required: false,
remotes: remote_runtime_connection_summaries(api, runtime_config, false),
diagnostics,
}
}
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,
runtime_config: &BackendRuntimesConfigFile,
restart_required: bool,
) -> Vec<RemoteRuntimeConnectionSummary> {
let live_runtimes = api
.runtime
.list_runtimes(api.config.max_records.min(200))
.items;
runtime_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(())
}
fn remote_runtime_config_from_file(
remote: &RemoteRuntimeConfigFile,
) -> std::result::Result<RemoteRuntimeConfig, RuntimeDiagnostic> {
resolve_remote_runtime(remote).map_err(|err| {
settings_diagnostic(
"remote_runtime_apply_failed",
DiagnosticSeverity::Error,
err.to_string(),
)
})
}
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 client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()
{
Ok(client) => client,
Err(_) => {
return remote_runtime_test_failed(
api,
remote,
checked_at,
"remote_runtime_test_client_unavailable",
"Remote Runtime test client could not be initialized.",
);
}
};
let mut observation = RuntimeCompatibilityObservation::default();
let summary_url = match remote_probe_url(remote, "/v1/runtime") {
Ok(url) => url,
Err(diagnostic) => {
return remote_runtime_test_failed(
api,
remote,
checked_at,
diagnostic.code,
diagnostic.message,
);
}
};
let summary_payload =
match probe_remote_json(&client, summary_url, "runtime.summary", "Runtime summary").await {
Ok(payload) => payload,
Err(diagnostic) => {
return remote_runtime_test_failed(
api,
remote,
checked_at,
diagnostic.code,
diagnostic.message,
);
}
};
let protocol_version = summary_payload
.get("protocol_version")
.and_then(|value| value.as_str())
.map(ToOwned::to_owned);
let summary = match serde_json::from_value::<RuntimeHttpSummaryResponse>(summary_payload) {
Ok(summary) => summary,
Err(_) => {
return remote_runtime_test_failed(
api,
remote,
checked_at,
"remote_runtime_malformed_summary",
"Remote Runtime summary responded, but the payload was not recognized.",
);
}
};
observation.available(
"runtime.summary",
"Connected: /v1/runtime responded with a recognized worker-runtime summary.",
);
let workers_url = match remote_probe_url(remote, "/v1/workers") {
Ok(url) => url,
Err(diagnostic) => {
observation.incompatible("workers.list", diagnostic);
String::new()
}
};
let workers = if workers_url.is_empty() {
None
} else {
match probe_remote_json(&client, workers_url, "workers.list", "Worker list").await {
Ok(payload) => match serde_json::from_value::<RuntimeHttpWorkersResponse>(payload) {
Ok(workers) => {
observation.available(
"workers.list",
"Verified: /v1/workers responded with a recognized worker list.",
);
Some(workers)
}
Err(_) => {
observation.incompatible(
"workers.list",
settings_diagnostic(
"remote_runtime_workers_malformed",
DiagnosticSeverity::Error,
"Remote Runtime worker list responded, but the payload was not recognized.",
),
);
None
}
},
Err(diagnostic) => {
observation.incompatible("workers.list", diagnostic);
None
}
}
};
if let Some(worker) = workers.as_ref().and_then(|workers| workers.workers.first()) {
let path = format!(
"/v1/workers/{}",
encode_path_segment(&worker.worker_id.to_string())
);
match remote_probe_url(remote, &path) {
Ok(url) => match probe_remote_json(&client, url, "workers.detail", "Worker detail").await {
Ok(payload) => match serde_json::from_value::<RuntimeHttpWorkerResponse>(payload) {
Ok(_) => observation.available(
"workers.detail",
"Verified: worker detail responded for an existing worker reported by the remote Runtime.",
),
Err(_) => observation.incompatible(
"workers.detail",
settings_diagnostic(
"remote_runtime_worker_detail_malformed",
DiagnosticSeverity::Error,
"Remote Runtime worker detail responded, but the payload was not recognized.",
),
),
},
Err(diagnostic) => observation.incompatible("workers.detail", diagnostic),
},
Err(diagnostic) => observation.incompatible("workers.detail", diagnostic),
}
} else {
observation.unknown(
"workers.detail",
"No connection problem found. Worker detail was not checked because the remote Runtime reported no workers during the lightweight probe.",
);
}
observation.available(
"workers.events_ws.construct",
"Verified: worker event websocket URL can be constructed from the configured HTTP(S) Runtime endpoint. The lightweight test does not open a websocket stream.",
);
let bundles_url = match remote_probe_url(remote, "/v1/config-bundles") {
Ok(url) => url,
Err(diagnostic) => {
observation.incompatible("config_bundles.list", diagnostic);
String::new()
}
};
let bundles = if bundles_url.is_empty() {
None
} else {
match probe_remote_json(
&client,
bundles_url,
"config_bundles.list",
"Config-bundle list",
)
.await
{
Ok(payload) => {
match serde_json::from_value::<RuntimeHttpConfigBundlesResponse>(payload) {
Ok(bundles) => {
observation.available(
"config_bundles.list",
"Verified: /v1/config-bundles responded with a recognized config-bundle list.",
);
Some(bundles)
}
Err(_) => {
observation.incompatible(
"config_bundles.list",
settings_diagnostic(
"remote_runtime_config_bundles_malformed",
DiagnosticSeverity::Error,
"Remote Runtime config-bundle list responded, but the payload was not recognized.",
),
);
None
}
}
}
Err(diagnostic) => {
observation.incompatible("config_bundles.list", diagnostic);
None
}
}
};
if let Some(bundle) = bundles.as_ref().and_then(|bundles| bundles.bundles.first()) {
let path = format!(
"/v1/config-bundles/{}/availability?digest={}",
encode_path_segment(&bundle.id),
encode_path_segment(&bundle.digest)
);
match remote_probe_url(remote, &path) {
Ok(url) => match probe_remote_json(
&client,
url,
"config_bundles.availability",
"Config-bundle availability",
)
.await
{
Ok(payload) => {
match serde_json::from_value::<RuntimeHttpConfigBundleAvailabilityResponse>(payload)
{
Ok(_) => observation.available(
"config_bundles.availability",
"Verified: config-bundle availability was confirmed for an advertised bundle.",
),
Err(_) => observation.incompatible(
"config_bundles.availability",
settings_diagnostic(
"remote_runtime_config_bundle_availability_malformed",
DiagnosticSeverity::Error,
"Remote Runtime config-bundle availability responded, but the payload was not recognized.",
),
),
}
}
Err(diagnostic) => {
observation.incompatible("config_bundles.availability", diagnostic)
}
},
Err(diagnostic) => observation.incompatible("config_bundles.availability", diagnostic),
}
} else {
observation.unknown(
"config_bundles.availability",
"No connection problem found. Config-bundle availability was not checked because the remote Runtime advertised no bundles during the lightweight probe.",
);
}
if summary.runtime.worker_creation_available {
observation.available(
"workers.spawn",
"Verified: /v1/runtime reports worker creation is enabled by a Runtime execution backend. The lightweight test does not create a worker.",
);
} else {
observation.incompatible(
"workers.spawn",
settings_diagnostic(
"remote_runtime_worker_creation_unavailable",
DiagnosticSeverity::Error,
"Connected to the Runtime, but worker creation is unavailable because this Runtime process has no execution backend attached.",
),
);
}
observation.unknown(
"workers.input_dispatch",
"No connection problem found. Worker input dispatch was not checked because this lightweight test does not send model-visible input as a side effect.",
);
observation.unknown(
"config_bundles.sync",
"No connection problem found. Config-bundle sync was not checked because this lightweight test does not upload bundles as a side effect.",
);
RemoteRuntimeTestResponse {
workspace_id: api.config.workspace_id.clone(),
runtime_id: remote.id.clone(),
checked_at,
state: observation.state().to_string(),
protocol_version,
compatibility_basis: "Connected to /v1/runtime and verified non-side-effecting worker-runtime HTTP endpoints. No incompatible operation was found; warning items below are unproven optional or side-effecting checks, not connection failures.".to_string(),
capabilities: observation.capabilities,
health_result: format!(
"connected=true; runtime_status={:?}; available={}; incompatible={}; warnings={}",
summary.runtime.status,
observation.available_count,
observation.incompatible_count,
observation.unknown_count
),
diagnostics: observation.diagnostics,
}
}
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 lightweight HTTP compatibility probes".to_string(),
capabilities: Vec::new(),
health_result: "failed".to_string(),
diagnostics: vec![settings_diagnostic(
code,
DiagnosticSeverity::Error,
message,
)],
}
}
#[derive(Default)]
struct RuntimeCompatibilityObservation {
capabilities: Vec<String>,
diagnostics: Vec<RuntimeDiagnostic>,
available_count: usize,
incompatible_count: usize,
unknown_count: usize,
}
impl RuntimeCompatibilityObservation {
fn available(&mut self, operation: &str, message: impl Into<String>) {
self.available_count += 1;
self.capabilities.push(format!("{operation}:available"));
self.diagnostics.push(settings_diagnostic(
format!("{operation}.available"),
DiagnosticSeverity::Info,
message,
));
}
fn unknown(&mut self, operation: &str, message: impl Into<String>) {
self.unknown_count += 1;
self.capabilities.push(format!("{operation}:unknown"));
self.diagnostics.push(settings_diagnostic(
format!("{operation}.unknown"),
DiagnosticSeverity::Warning,
message,
));
}
fn incompatible(&mut self, operation: &str, diagnostic: RuntimeDiagnostic) {
self.incompatible_count += 1;
self.capabilities.push(format!("{operation}:incompatible"));
self.diagnostics.push(diagnostic);
}
fn state(&self) -> &'static str {
if self.incompatible_count > 0 {
"incompatible"
} else {
"compatible"
}
}
}
fn remote_probe_url(
remote: &RemoteRuntimeConfigFile,
path: &str,
) -> std::result::Result<String, RuntimeDiagnostic> {
let endpoint = remote.endpoint.trim();
if !(endpoint.starts_with("http://") || endpoint.starts_with("https://")) {
return Err(settings_diagnostic(
"remote_runtime_endpoint_invalid",
DiagnosticSeverity::Error,
"Configured remote Runtime endpoint is not an absolute HTTP(S) URL.",
));
}
Ok(format!("{}{}", endpoint.trim_end_matches('/'), path))
}
async fn probe_remote_json(
client: &reqwest::Client,
url: String,
operation: &'static str,
label: &'static str,
) -> std::result::Result<serde_json::Value, RuntimeDiagnostic> {
let response = client.get(url).send().await.map_err(|error| {
let (code, message) = if error.is_timeout() {
(
format!("{operation}.timeout"),
format!("Remote Runtime probe for {label} timed out."),
)
} else if error.is_connect() {
(
format!("{operation}.connect_failed"),
format!("Remote Runtime probe for {label} could not connect."),
)
} else {
(
format!("{operation}.request_failed"),
format!("Remote Runtime probe for {label} failed before a response was received."),
)
};
settings_diagnostic(code, DiagnosticSeverity::Error, message)
})?;
if !response.status().is_success() {
return Err(settings_diagnostic(
format!("{operation}.http_status"),
DiagnosticSeverity::Error,
format!(
"Remote Runtime probe for {label} returned HTTP status {}.",
response.status().as_u16()
),
));
}
response.json::<serde_json::Value>().await.map_err(|_| {
settings_diagnostic(
format!("{operation}.malformed_json"),
DiagnosticSeverity::Error,
format!("Remote Runtime probe for {label} returned an unrecognized JSON payload."),
)
})
}
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,
working_directory_required: !built_in,
status: runtime.status,
diagnostics: runtime.diagnostics,
}
})
.collect();
let profile_settings = crate::profile_settings::load_profile_settings(
&api.config.workspace_id,
&api.config.workspace_root,
);
let profiles = profile_settings
.profiles
.into_iter()
.filter(|profile| {
!profile
.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error)
})
.map(|profile| WorkerLaunchProfileCandidate {
id: profile.profile_id,
label: profile.label,
description: profile
.description
.unwrap_or_else(|| "Workspace profile.".to_string()),
})
.collect();
WorkerLaunchOptionsResponse {
workspace_id: api.config.workspace_id.clone(),
runtimes,
default_profile: profile_settings.default_profile,
profiles,
repositories: working_directory_repository_options(api),
working_directories: available_working_directory_summaries(api).unwrap_or_default(),
diagnostics: Vec::new(),
}
}
fn working_directory_repository_options(
api: &WorkspaceApi,
) -> Vec<WorkingDirectoryRepositoryOption> {
api.config
.repositories
.iter()
.map(|repository| WorkingDirectoryRepositoryOption {
id: repository.id.clone(),
display_name: repository
.display_name
.clone()
.unwrap_or_else(|| repository.id.clone()),
default_selector: repository.default_selector.clone(),
})
.collect()
}
fn working_directory_summaries(api: &WorkspaceApi) -> ApiResult<Vec<WorkingDirectorySummary>> {
let _ = sync_all_runtime_workdir_observations(api);
let records = api
.store
.list_workdir_registry(&api.config.workspace_id, 200)?;
records
.iter()
.map(|record| projected_workdir_summary_from_record(api, record))
.collect::<Result<Vec<_>>>()
.map_err(ApiError::from)
}
fn available_working_directory_summaries(
api: &WorkspaceApi,
) -> ApiResult<Vec<WorkingDirectorySummary>> {
let limit = api.config.max_records.min(200);
for worker in api.runtime.list_workers(limit).items {
let _ = sync_worker_observation(api, &worker);
}
let records = working_directory_summaries(api)?;
let mut available = Vec::new();
for summary in records {
if summary.status != WorkingDirectoryStatusKind::Active
|| summary.cleanliness.as_deref() != Some("clean")
{
continue;
}
if summary.occupied_by.is_none() && summary.primary_worker_id.is_none() {
available.push(summary);
}
}
Ok(available)
}
fn runtime_working_directory_summaries(
api: &WorkspaceApi,
runtime_id: &str,
) -> ApiResult<(Vec<WorkingDirectorySummary>, Vec<RuntimeDiagnostic>)> {
let diagnostics = sync_runtime_workdir_observations(api, runtime_id)?;
let records = api
.store
.list_workdir_registry(&api.config.workspace_id, 200)?;
let items = records
.iter()
.filter(|record| record.runtime_id == runtime_id)
.map(|record| projected_workdir_summary_from_record(api, record))
.collect::<Result<Vec<_>>>()
.map_err(ApiError::from)?;
Ok((items, diagnostics))
}
static BACKEND_WORKDIR_ID_SEQUENCE: AtomicU64 = AtomicU64::new(0);
fn now_registry_timestamp() -> String {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_millis().to_string())
.unwrap_or_else(|_| "0".to_string())
}
fn next_backend_workdir_id(_repository_id: &str) -> String {
let timestamp_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.unwrap_or(0);
let sequence = BACKEND_WORKDIR_ID_SEQUENCE.fetch_add(1, Ordering::Relaxed) & 0x00ff_ffff;
format!("{timestamp_ms:013x}{sequence:06x}")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum WorkerRegistryDisplayNamePolicy {
PreserveExisting,
UseProvided,
}
fn record_worker_summary(
api: &WorkspaceApi,
worker: &WorkerSummary,
display_name: &str,
profile: Option<String>,
display_name_policy: WorkerRegistryDisplayNamePolicy,
) -> ApiResult<WorkerRegistryRecord> {
let timestamp = now_registry_timestamp();
let runtime_worker_id = parse_runtime_worker_id_for_registry(worker.worker_id.as_str())?;
let existing = api.store.get_worker_registry(
&api.config.workspace_id,
worker.runtime_id.as_str(),
runtime_worker_id,
)?;
let display_name = match (display_name_policy, existing.as_ref()) {
(WorkerRegistryDisplayNamePolicy::PreserveExisting, Some(record)) => {
record.display_name.clone()
}
_ => display_name.to_string(),
};
let record = WorkerRegistryRecord {
workspace_id: api.config.workspace_id.clone(),
runtime_id: worker.runtime_id.as_str().to_string(),
runtime_worker_id,
display_name,
profile,
retention_state: existing
.as_ref()
.map(|record| record.retention_state.clone())
.unwrap_or_else(|| "normal".to_string()),
transcript_ref: Some(format!(
"runtime://{}/workers/{}/transcript",
worker.runtime_id.as_str(),
worker.worker_id.as_str()
)),
session_ref: None,
summary_ref: None,
diagnostics_ref: None,
created_at: timestamp.clone(),
updated_at: timestamp,
};
api.store.upsert_worker_registry(&record)?;
Ok(api
.store
.get_worker_registry(
&api.config.workspace_id,
worker.runtime_id.as_str(),
runtime_worker_id,
)?
.unwrap_or(record))
}
fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> WorkerSummary {
WorkerSummary {
worker_id: record.runtime_worker_id.to_string(),
runtime_id: record.runtime_id.clone(),
host_id: "backend-registry".to_string(),
display_name: record.display_name.clone(),
label: record.display_name.clone(),
singleton_key: None,
tags: Vec::new(),
state: "missing".to_string(),
last_seen_at: Some(record.updated_at.clone()),
pinned: record.retention_state == "pinned",
retention_state: record.retention_state.clone(),
capabilities: WorkerCapabilitySummary {
can_stop: false,
can_spawn_followup: false,
},
workspace: WorkerWorkspaceSummary {
visibility: "backend_registry".to_string(),
identity: record.workspace_id.clone(),
},
profile: record.profile.clone(),
implementation: WorkerImplementationSummary {
kind: "backend_worker_registry".to_string(),
display_hint: "Missing Worker".to_string(),
},
working_directory: None,
diagnostics: vec![RuntimeDiagnostic {
code: "backend_worker_missing".to_string(),
severity: DiagnosticSeverity::Info,
message:
"Worker is preserved in the Backend registry but the Runtime did not find it by id"
.to_string(),
}],
}
}
fn merge_worker_registry_projection(
live: Option<&WorkerSummary>,
record: &WorkerRegistryRecord,
links: Vec<WorkerWorkdirLinkRecord>,
workdirs: &[WorkdirRegistryRecord],
) -> WorkerSummary {
let mut summary = live
.cloned()
.unwrap_or_else(|| worker_summary_from_registry(record));
summary.label = record.display_name.clone();
summary.profile = record.profile.clone();
summary.pinned = record.retention_state == "pinned";
summary.retention_state = record.retention_state.clone();
summary.working_directory = links.iter().find_map(|link| {
workdirs
.iter()
.find(|workdir| workdir.workdir_id == link.workdir_id)
.map(|workdir| {
let mut workdir_summary = workdir_summary_from_record(workdir);
workdir_summary.primary_worker_id = Some(WorkerId::new(record.runtime_worker_id));
workdir_summary.occupied_by = Some(WorkingDirectoryOccupancy {
runtime_id: record.runtime_id.clone(),
runtime_worker_id: record.runtime_worker_id,
worker_id: format!("{}:{}", record.runtime_id, record.runtime_worker_id),
display_name: record.display_name.clone(),
linked_at: link.linked_at.clone(),
});
workdir_summary
})
});
summary
}
fn sync_worker_observation(
api: &WorkspaceApi,
worker: &WorkerSummary,
) -> ApiResult<WorkerRegistryRecord> {
let record = record_worker_summary(
api,
worker,
worker.label.as_str(),
worker.profile.clone(),
WorkerRegistryDisplayNamePolicy::PreserveExisting,
)?;
if let Some(working_directory) = worker.working_directory.as_ref() {
let workdir_record =
workdir_record_from_summary(api, worker.runtime_id.as_str(), working_directory);
api.store.upsert_workdir_registry(&workdir_record)?;
link_worker_to_workdir(api, &record, &working_directory.working_directory_id)?;
}
Ok(record)
}
fn upsert_pending_backend_workdir(
api: &WorkspaceApi,
runtime_id: &str,
request: &mut WorkingDirectoryRequest,
) -> ApiResult<String> {
let workdir_id = request
.backend_workdir_id
.clone()
.unwrap_or_else(|| next_backend_workdir_id(&request.repository.id));
request.backend_workdir_id = Some(workdir_id.clone());
let timestamp = now_registry_timestamp();
api.store.upsert_workdir_registry(&WorkdirRegistryRecord {
workspace_id: api.config.workspace_id.clone(),
workdir_id: workdir_id.clone(),
runtime_id: runtime_id.to_string(),
repository_id: request.repository.id.clone(),
creation_selector: request
.repository
.selector
.as_ref()
.map(|selector| selector.as_ref().to_string()),
creation_ref: None,
current_selector: None,
current_ref: None,
materialization_status: "pending".to_string(),
cleanliness: "unknown".to_string(),
created_at: timestamp.clone(),
updated_at: timestamp,
})?;
Ok(workdir_id)
}
fn sync_runtime_workdir_observations(
api: &WorkspaceApi,
runtime_id: &str,
) -> ApiResult<Vec<RuntimeDiagnostic>> {
let response = api
.runtime
.list_working_directories(runtime_id)
.map_err(|err| err.into_error())?;
let mut observed = std::collections::BTreeSet::new();
for status in &response.items {
observed.insert(status.summary.working_directory_id.clone());
if status.summary.status == WorkingDirectoryStatusKind::NotFound {
api.store.delete_workdir_registry(
&api.config.workspace_id,
&status.summary.working_directory_id,
)?;
continue;
}
let existing = api.store.get_workdir_registry(
&api.config.workspace_id,
&status.summary.working_directory_id,
)?;
let mut record = workdir_record_from_summary(api, runtime_id, &status.summary);
preserve_workdir_identity_for_corrupted_summary(&mut record, existing.as_ref());
api.store.upsert_workdir_registry(&record)?;
}
for mut record in api
.store
.list_workdir_registry(&api.config.workspace_id, 500)?
.into_iter()
.filter(|record| record.runtime_id == runtime_id && !observed.contains(&record.workdir_id))
{
match api
.runtime
.working_directory(runtime_id, record.workdir_id.as_str())
{
Ok(result) => {
if let Some(status) = result.working_directory {
if status.summary.status == WorkingDirectoryStatusKind::NotFound {
api.store.delete_workdir_registry(
&api.config.workspace_id,
record.workdir_id.as_str(),
)?;
} else {
let mut updated =
workdir_record_from_summary(api, runtime_id, &status.summary);
preserve_workdir_identity_for_corrupted_summary(
&mut updated,
Some(&record),
);
api.store.upsert_workdir_registry(&updated)?;
}
} else {
record.materialization_status =
workdir_status_from_runtime_miss(result.diagnostics.as_slice()).to_string();
record.cleanliness = "unknown".to_string();
record.updated_at = now_registry_timestamp();
api.store.upsert_workdir_registry(&record)?;
}
}
Err(_) => {
record.materialization_status = "unknown".to_string();
record.cleanliness = "unknown".to_string();
record.updated_at = now_registry_timestamp();
api.store.upsert_workdir_registry(&record)?;
}
}
}
Ok(response.diagnostics)
}
fn workdir_status_from_runtime_miss(diagnostics: &[RuntimeDiagnostic]) -> &'static str {
if diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "working_directory_not_found")
{
"not_found"
} else {
"unknown"
}
}
fn sync_all_runtime_workdir_observations(api: &WorkspaceApi) -> Vec<RuntimeDiagnostic> {
let mut diagnostics = Vec::new();
let runtimes = api.runtime.list_runtimes(api.config.max_records.min(200));
for runtime in runtimes.items {
if runtime.capabilities.supports_worktrees {
match sync_runtime_workdir_observations(api, runtime.runtime_id.as_str()) {
Ok(mut runtime_diagnostics) => diagnostics.append(&mut runtime_diagnostics),
Err(err) => diagnostics.extend(err.diagnostics),
}
}
}
diagnostics
}
fn sync_linked_workdir_after_worker_stop(
api: &WorkspaceApi,
runtime_id: &str,
worker_record: &WorkerRegistryRecord,
) -> ApiResult<()> {
let links = api.store.list_worker_workdir_links(
&api.config.workspace_id,
worker_record.runtime_id.as_str(),
worker_record.runtime_worker_id,
)?;
for link in links {
let result = api
.runtime
.working_directory(runtime_id, link.workdir_id.as_str())
.map_err(|err| err.into_error())?;
if let Some(status) = result.working_directory {
let record = workdir_record_from_summary(api, runtime_id, &status.summary);
api.store.upsert_workdir_registry(&record)?;
} else if let Some(mut record) = api
.store
.get_workdir_registry(&api.config.workspace_id, link.workdir_id.as_str())?
{
record.materialization_status =
workdir_status_from_runtime_miss(result.diagnostics.as_slice()).to_string();
record.cleanliness = "unknown".to_string();
record.updated_at = now_registry_timestamp();
api.store.upsert_workdir_registry(&record)?;
}
}
Ok(())
}
fn workdir_record_from_summary(
api: &WorkspaceApi,
runtime_id: &str,
summary: &WorkingDirectorySummary,
) -> WorkdirRegistryRecord {
let timestamp = now_registry_timestamp();
WorkdirRegistryRecord {
workspace_id: api.config.workspace_id.clone(),
workdir_id: summary.working_directory_id.clone(),
runtime_id: runtime_id.to_string(),
repository_id: summary.repository_id.clone(),
creation_selector: summary.creation_selector.clone(),
creation_ref: summary.creation_ref.clone(),
current_selector: summary.current_selector.clone(),
current_ref: summary.current_ref.clone(),
materialization_status: match summary.status {
WorkingDirectoryStatusKind::Active => "present",
WorkingDirectoryStatusKind::CleanupPending => "pending",
WorkingDirectoryStatusKind::Corrupted => "corrupted",
WorkingDirectoryStatusKind::NotFound => "not_found",
WorkingDirectoryStatusKind::Unknown => "unknown",
}
.to_string(),
cleanliness: summary
.cleanliness
.clone()
.unwrap_or_else(|| "unknown".to_string()),
created_at: timestamp.clone(),
updated_at: timestamp,
}
}
fn preserve_workdir_identity_for_corrupted_summary(
record: &mut WorkdirRegistryRecord,
existing: Option<&WorkdirRegistryRecord>,
) {
if record.materialization_status != "corrupted" {
return;
}
let Some(existing) = existing else {
return;
};
if record.repository_id == "unknown" {
record.repository_id = existing.repository_id.clone();
}
if record.creation_selector.is_none() {
record.creation_selector = existing.creation_selector.clone();
}
if record.creation_ref.is_none() {
record.creation_ref = existing.creation_ref.clone();
}
}
fn workdir_summary_from_record(record: &WorkdirRegistryRecord) -> WorkingDirectorySummary {
let status = match record.materialization_status.as_str() {
"present" => WorkingDirectoryStatusKind::Active,
"pending" => WorkingDirectoryStatusKind::CleanupPending,
"corrupted" => WorkingDirectoryStatusKind::Corrupted,
"not_found" | "missing" => WorkingDirectoryStatusKind::NotFound,
"unknown" => WorkingDirectoryStatusKind::Unknown,
_ => WorkingDirectoryStatusKind::Unknown,
};
WorkingDirectorySummary {
working_directory_id: record.workdir_id.clone(),
repository_id: record.repository_id.clone(),
creation_selector: record.creation_selector.clone(),
creation_ref: record.creation_ref.clone(),
current_selector: record.current_selector.clone(),
current_ref: record.current_ref.clone(),
materializer_kind: MaterializerKind::LocalGitWorktree,
cleanup_target: Some(worker_runtime::catalog::WorkingDirectoryCleanupTarget {
kind: "local_git_worktree".to_string(),
working_directory_id: record.workdir_id.clone(),
repository_id: record.repository_id.clone(),
}),
status,
cleanliness: Some(record.cleanliness.clone()),
primary_worker_id: None,
occupied_by: None,
}
}
fn apply_workdir_occupancy_projection(
api: &WorkspaceApi,
summary: &mut WorkingDirectorySummary,
) -> Result<()> {
let links = api
.store
.list_workdir_worker_links(&api.config.workspace_id, &summary.working_directory_id)?;
let Some(link) = links.first() else {
summary.primary_worker_id = None;
summary.occupied_by = None;
return Ok(());
};
let worker = api.store.get_worker_registry(
&api.config.workspace_id,
&link.runtime_id,
link.runtime_worker_id,
)?;
let display_name = worker
.as_ref()
.map(|worker| worker.display_name.clone())
.unwrap_or_else(|| format!("{}:{}", link.runtime_id, link.runtime_worker_id));
summary.primary_worker_id = Some(WorkerId::new(link.runtime_worker_id));
summary.occupied_by = Some(WorkingDirectoryOccupancy {
runtime_id: link.runtime_id.clone(),
runtime_worker_id: link.runtime_worker_id,
worker_id: format!("{}:{}", link.runtime_id, link.runtime_worker_id),
display_name,
linked_at: link.linked_at.clone(),
});
Ok(())
}
fn projected_workdir_summary_from_record(
api: &WorkspaceApi,
record: &WorkdirRegistryRecord,
) -> Result<WorkingDirectorySummary> {
let mut summary = workdir_summary_from_record(record);
apply_workdir_occupancy_projection(api, &mut summary)?;
Ok(summary)
}
fn link_worker_to_workdir(
api: &WorkspaceApi,
worker_record: &WorkerRegistryRecord,
workdir_id: &str,
) -> ApiResult<()> {
let timestamp = now_registry_timestamp();
api.store
.upsert_worker_workdir_link(&WorkerWorkdirLinkRecord {
workspace_id: api.config.workspace_id.clone(),
runtime_id: worker_record.runtime_id.clone(),
runtime_worker_id: worker_record.runtime_worker_id.clone(),
workdir_id: workdir_id.to_string(),
role: "primary_cwd".to_string(),
linked_at: timestamp,
unlinked_at: None,
})?;
Ok(())
}
fn validate_working_directory_claim_for_browser(
claim: Option<&WorkingDirectoryClaim>,
) -> ApiResult<()> {
let Some(claim) = claim else {
return Ok(());
};
if let Some(relative_cwd) = claim.relative_cwd.as_deref() {
let path = Path::new(relative_cwd);
if path.is_absolute()
|| path
.components()
.any(|component| !matches!(component, Component::CurDir | Component::Normal(_)))
{
return Err(ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
code: "working_directory_relative_cwd_invalid".to_string(),
message: "working directory relative_cwd must stay inside the Runtime-owned working directory".to_string(),
},
vec![RuntimeDiagnostic {
code: "working_directory_relative_cwd_invalid".to_string(),
severity: DiagnosticSeverity::Error,
message: "relative_cwd must be a relative path without parent traversal".to_string(),
}],
));
}
}
Ok(())
}
fn working_directory_request_for_browser(
api: &WorkspaceApi,
request: BrowserWorkingDirectoryCreateRequest,
) -> ApiResult<WorkingDirectoryRequest> {
let repository = api
.config
.repositories
.iter()
.find(|repository| repository.id == request.repository_id)
.ok_or_else(|| Error::UnknownRepository(request.repository_id.clone()))?;
let selector = request
.selector
.or_else(|| repository.default_selector.clone())
.filter(|selector| !selector.trim().is_empty());
Ok(WorkingDirectoryRequest {
repository: WorkingDirectoryRepository {
id: repository.id.clone(),
provider: "git".to_string(),
uri: repository.path.to_string_lossy().to_string(),
local_path: Some(repository.path.clone()),
selector: selector.map(RuntimeRepositorySelector),
},
materializer: MaterializerKind::LocalGitWorktree,
backend_workdir_id: None,
})
}
fn profile_selector_for_candidate_with_root(
workspace_root: &Path,
profile: &str,
) -> Option<ProfileSelector> {
if let Some(selector @ ProfileSelector::Builtin(_)) =
crate::profile_settings::selector_for_builtin_candidate(profile)
{
return Some(selector);
}
crate::profile_settings::project_profile_candidates(workspace_root)
.into_iter()
.find(|candidate| {
candidate.profile_id == profile
&& !candidate
.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error)
})
.and_then(|_| crate::profile_settings::selector_for_builtin_candidate(profile))
}
fn parse_runtime_worker_id_for_registry(worker_id: &str) -> ApiResult<u64> {
worker_id.parse::<u64>().map_err(|_| {
settings_bad_request(
"workspace_worker_id_invalid",
"Runtime Worker id must be an unsigned integer",
)
})
}
fn sanitize_worker_display_name(value: &str) -> Option<String> {
let display_name = value.trim();
if display_name.chars().any(char::is_control) {
None
} else if display_name.is_empty() {
Some("Worker".to_string())
} 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 worker_create_not_accepted_error(
runtime_id: String,
mut diagnostics: Vec<RuntimeDiagnostic>,
) -> ApiError {
diagnostics.push(settings_diagnostic(
"workspace_worker_create_not_accepted",
DiagnosticSeverity::Error,
"Runtime did not accept worker creation; see diagnostics for sanitized Runtime compatibility details.",
));
ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id,
code: "workspace_worker_create_failed".to_string(),
message: "Runtime did not accept worker creation".to_string(),
},
diagnostics,
)
}
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 {
message.to_string()
}
fn repository_diagnostics(
diagnostics: Vec<crate::repositories::RepositoryDiagnostic>,
) -> Vec<RuntimeDiagnostic> {
diagnostics
.into_iter()
.map(|diagnostic| RuntimeDiagnostic {
code: diagnostic.code,
severity: match diagnostic.severity.as_str() {
"error" => DiagnosticSeverity::Error,
"warning" => DiagnosticSeverity::Warning,
_ => DiagnosticSeverity::Info,
},
message: diagnostic.message,
})
.collect()
}
fn repository_lookup<T>(result: std::result::Result<T, RepositoryLookupError>) -> ApiResult<T> {
result.map_err(|error| match error {
RepositoryLookupError::UnknownRepository { id } => {
let message = format!("repository `{id}` is not configured for this workspace");
ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: "workspace-repository-registry".to_string(),
code: "repository_not_configured".to_string(),
message: message.clone(),
},
vec![RuntimeDiagnostic {
code: "repository_not_configured".to_string(),
severity: DiagnosticSeverity::Error,
message,
}],
)
}
RepositoryLookupError::UnsupportedProvider { id, provider } => {
let message = format!(
"repository `{id}` uses unsupported provider `{provider}` for this operation"
);
ApiError::with_diagnostics(
Error::RuntimeOperationFailed {
runtime_id: "workspace-repository-registry".to_string(),
code: "repository_provider_unsupported".to_string(),
message: message.clone(),
},
vec![RuntimeDiagnostic {
code: "repository_provider_unsupported".to_string(),
severity: DiagnosticSeverity::Error,
message,
}],
)
}
})
}
async fn static_or_spa_fallback(State(api): State<WorkspaceApi>, uri: Uri) -> Response {
if uri.path().starts_with("/api/") || uri.path() == "/api" {
return (
StatusCode::NOT_FOUND,
[(CONTENT_TYPE, "application/json")],
Json(serde_json::json!({
"error": "not_found",
"message": "unknown api route"
}))
.to_string(),
)
.into_response();
}
if let Some(workspace_id) = workspace_id_from_ui_path(uri.path()) {
if workspace_id != api.workspace_id() {
return workspace_id_mismatch_error().into_response();
}
}
if let Some(location) =
unscoped_workspace_ui_redirect(uri.path(), uri.query(), api.workspace_id())
{
return (StatusCode::TEMPORARY_REDIRECT, [(LOCATION, location)]).into_response();
}
let Some(static_root) = api.config.static_assets_dir.as_ref() else {
return StatusCode::NOT_FOUND.into_response();
};
match read_static_or_index(static_root, uri.path()).await {
Ok(StaticAsset {
bytes,
content_type,
}) => (StatusCode::OK, [(CONTENT_TYPE, content_type)], bytes).into_response(),
Err(error) => {
tracing::debug!(%error, path = %uri.path(), "failed to serve static asset");
StatusCode::NOT_FOUND.into_response()
}
}
}
fn unscoped_workspace_ui_redirect(
path: &str,
query: Option<&str>,
workspace_id: &str,
) -> Option<String> {
let scoped_tail = if path == "/" {
""
} else if ["/repositories", "/objectives", "/settings", "/runtimes"]
.iter()
.any(|prefix| path == *prefix || path.starts_with(&format!("{prefix}/")))
{
path
} else {
return None;
};
let mut location = format!("/w/{}{}", encode_path_segment(workspace_id), scoped_tail);
if let Some(query) = query.filter(|query| !query.is_empty()) {
location.push('?');
location.push_str(query);
}
Some(location)
}
fn workspace_id_from_ui_path(path: &str) -> Option<&str> {
let tail = path.strip_prefix("/w/")?;
let workspace_id = tail.split('/').next().unwrap_or_default();
if workspace_id.is_empty() {
None
} else {
Some(workspace_id)
}
}
struct StaticAsset {
bytes: Vec<u8>,
content_type: &'static str,
}
async fn read_static_or_index(root: &Path, request_path: &str) -> Result<StaticAsset> {
let candidate = safe_static_candidate(root, request_path)?;
let file = if tokio::fs::metadata(&candidate)
.await
.map(|m| m.is_file())
.unwrap_or(false)
{
candidate
} else {
root.join("index.html")
};
let content_type = content_type_for(&file);
let bytes = tokio::fs::read(file).await?;
Ok(StaticAsset {
bytes,
content_type,
})
}
fn safe_static_candidate(root: &Path, request_path: &str) -> Result<PathBuf> {
let mut path = root.to_path_buf();
let clean = request_path.trim_start_matches('/');
if clean.is_empty() {
path.push("index.html");
return Ok(path);
}
for component in Path::new(clean).components() {
match component {
Component::Normal(part) => path.push(part),
Component::CurDir => {}
_ => return Err(Error::Store("static path escape rejected".to_string())),
}
}
Ok(path)
}
fn content_type_for(path: &Path) -> &'static str {
match path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or_default()
{
"css" => "text/css; charset=utf-8",
"js" => "text/javascript; charset=utf-8",
"json" => "application/json",
"svg" => "image/svg+xml",
"html" | "" => "text/html; charset=utf-8",
_ => "application/octet-stream",
}
}
type ApiResult<T> = std::result::Result<T, ApiError>;
fn workspace_id_mismatch_error() -> ApiError {
let message = "workspace id does not match this Workspace backend".to_string();
ApiError::with_diagnostics(
Error::WorkspaceIdMismatch,
vec![RuntimeDiagnostic {
code: "workspace_id_mismatch".to_string(),
severity: DiagnosticSeverity::Error,
message,
}],
)
}
#[derive(Debug)]
struct ApiError {
error: Error,
diagnostics: Vec<RuntimeDiagnostic>,
}
impl From<Error> for ApiError {
fn from(error: Error) -> Self {
let diagnostics = match &error {
Error::RuntimeOperationFailed { code, message, .. } => vec![RuntimeDiagnostic {
code: code.clone(),
severity: DiagnosticSeverity::Error,
message: sanitize_backend_error(message),
}],
Error::Ticket(ticket_error) => vec![RuntimeDiagnostic {
code: match ticket_error {
ticket::TicketError::NotFound(_) => "ticket_not_found",
ticket::TicketError::Ambiguous { .. } => "ticket_ambiguous",
ticket::TicketError::Locked { .. } => "ticket_locked",
ticket::TicketError::Conflict(_) => "ticket_conflict",
ticket::TicketError::InvalidPathComponent(_)
| ticket::TicketError::PathEscapesRoot { .. } => "invalid_ticket_request",
ticket::TicketError::Io { .. }
| ticket::TicketError::Parse { .. }
| ticket::TicketError::Sqlite(_) => "ticket_backend_error",
}
.to_string(),
severity: DiagnosticSeverity::Error,
message: sanitize_backend_error(&ticket_error.to_string()),
}],
_ => Vec::new(),
};
Self { error, diagnostics }
}
}
impl ApiError {
fn with_diagnostics(error: Error, diagnostics: Vec<RuntimeDiagnostic>) -> Self {
Self { error, diagnostics }
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let status = match &self.error {
Error::TicketAssignmentConflict(_) => StatusCode::CONFLICT,
Error::WorkerSourceIdentity(_) => StatusCode::BAD_REQUEST,
Error::InvalidRuntimeIdentifier { .. } | Error::ReservedWorkerName(_) => {
StatusCode::BAD_REQUEST
}
Error::Ticket(ticket::TicketError::NotFound(_)) => StatusCode::NOT_FOUND,
Error::Ticket(
ticket::TicketError::Ambiguous { .. }
| ticket::TicketError::Locked { .. }
| ticket::TicketError::Conflict(_),
) => StatusCode::CONFLICT,
Error::Ticket(
ticket::TicketError::InvalidPathComponent(_)
| ticket::TicketError::PathEscapesRoot { .. },
) => StatusCode::BAD_REQUEST,
Error::InvalidRecordId(_)
| Error::MissingFrontmatter(_)
| Error::UnknownHost(_)
| Error::UnknownRuntime(_)
| Error::UnknownWorker { .. }
| Error::UnknownRepository(_)
| Error::WorkspaceIdMismatch => StatusCode::NOT_FOUND,
Error::RuntimeOperationFailed { code, .. } if code == "skill_not_found" => {
StatusCode::NOT_FOUND
}
Error::RuntimeCapabilityUnsupported { .. } => StatusCode::NOT_IMPLEMENTED,
Error::RuntimeOperationFailed { code, .. } if code == "repository_not_configured" => {
StatusCode::NOT_FOUND
}
Error::RuntimeOperationFailed { code, .. }
if code == "repository_provider_unsupported" =>
{
StatusCode::BAD_REQUEST
}
Error::RuntimeOperationFailed { code, .. } if code == "remote_runtime_auth_failed" => {
StatusCode::UNAUTHORIZED
}
Error::RuntimeOperationFailed { code, .. } if code == "remote_runtime_timeout" => {
StatusCode::GATEWAY_TIMEOUT
}
Error::RuntimeOperationFailed { code, .. } if code == "remote_runtime_unsupported" => {
StatusCode::NOT_IMPLEMENTED
}
Error::RuntimeOperationFailed { code, .. }
if code == "profile_registry_revision_conflict"
|| code == "profile_source_revision_conflict"
|| code == "workspace_metadata_revision_conflict"
|| code == "workspace_cleanup_plan_stale"
|| code == "workspace_cleanup_worker_blocked"
|| code == "workspace_cleanup_workdir_blocked"
|| code == "workspace_cleanup_worker_pinned" =>
{
StatusCode::CONFLICT
}
Error::RuntimeOperationFailed { code, .. }
if code == "unknown_profile_source"
|| code == "unknown_profile_selector"
|| code == "unknown_objective" =>
{
StatusCode::NOT_FOUND
}
Error::RuntimeOperationFailed { code, .. }
if code == "workspace_display_name_invalid" || code.starts_with("profile_") =>
{
StatusCode::BAD_REQUEST
}
Error::RuntimeOperationFailed { code, .. } if code.ends_with("_blocked") => {
StatusCode::CONFLICT
}
Error::RuntimeOperationFailed { code, .. }
if code.starts_with("workspace_settings_")
|| code.starts_with("invalid_")
|| code.starts_with("unsupported_worker_profile")
|| code.starts_with("working_directory_")
|| code.starts_with("workspace_cleanup_")
|| code == "workspace_worker_workdir_required"
|| 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,
};
let response_message = match &self.error {
Error::RuntimeOperationFailed { code, message, .. } => {
format!("{code}: {}", sanitize_backend_error(message))
}
_ => sanitize_backend_error(&self.error.to_string()),
};
(
status,
[(CONTENT_TYPE, "application/json")],
Json(serde_json::json!({
"error": status.canonical_reason().unwrap_or("error"),
"message": response_message,
"diagnostics": self.diagnostics,
}))
.to_string(),
)
.into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::WorkspaceBackendRuntimesConfig;
use axum::body::{Body, to_bytes};
use axum::http::Request;
use futures::{SinkExt, StreamExt};
use serde_json::{Value, json};
use std::{fs, sync::Arc};
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;
use tower::ServiceExt;
use worker_runtime::resource::BackendResourceClient;
use worker_runtime::working_directory::WorkingDirectoryMaterializer;
use crate::hosts::{
TicketWorkerRole, WorkerInputKind, WorkerOperationState, WorkerSpawnAcceptanceRequirement,
WorkerSpawnIntent,
};
use crate::store::{
MemoryDocumentRecord, MemoryStagingRecord, ObjectiveRecord, ObjectiveResourceRecord,
ObjectiveTicketLinkRecord, SqliteWorkspaceStore, WorkspaceRecord,
};
const TEST_WORKSPACE_ID: &str = "0192f0e8-4d84-7d6e-a000-000000000001";
const TEST_REPOSITORY_ID: &str = "main";
const TEST_CREATED_AT: &str = "2026-06-23T06:43:28Z";
fn test_worker_workspace_api(runtime_id: &str) -> WorkspaceApiRef {
WorkspaceApiRef {
workspace_id: TEST_WORKSPACE_ID.to_string(),
base_url: "http://127.0.0.1:8787".to_string(),
runtime_id: Some(runtime_id.to_string()),
}
}
#[test]
fn ticket_api_errors_preserve_http_status() {
let not_found = ApiError::from(Error::Ticket(ticket::TicketError::NotFound(
"0000000000000".to_string(),
)))
.into_response();
assert_eq!(not_found.status(), StatusCode::NOT_FOUND);
let conflict = ApiError::from(Error::Ticket(ticket::TicketError::Conflict(
"invalid transition".to_string(),
)))
.into_response();
assert_eq!(conflict.status(), StatusCode::CONFLICT);
}
#[test]
fn backend_worker_projection_preserves_missing_rows_links_and_redacts_paths() {
let worker = WorkerRegistryRecord {
workspace_id: "workspace-1".to_string(),
runtime_id: "embedded".to_string(),
runtime_worker_id: 1,
display_name: "Missing Worker".to_string(),
profile: Some("builtin:coder".to_string()),
retention_state: "pinned".to_string(),
transcript_ref: Some("runtime://embedded/workers/worker-1/transcript".to_string()),
session_ref: None,
summary_ref: None,
diagnostics_ref: None,
created_at: "1".to_string(),
updated_at: "2".to_string(),
};
let workdir = WorkdirRegistryRecord {
workspace_id: "workspace-1".to_string(),
workdir_id: "0000019a00000000000".to_string(),
runtime_id: "embedded".to_string(),
repository_id: "repo".to_string(),
creation_selector: Some("develop".to_string()),
creation_ref: Some("abcdef".to_string()),
current_selector: None,
current_ref: Some("fedcba".to_string()),
materialization_status: "missing".to_string(),
cleanliness: "clean".to_string(),
created_at: "1".to_string(),
updated_at: "3".to_string(),
};
let link = WorkerWorkdirLinkRecord {
workspace_id: "workspace-1".to_string(),
runtime_id: worker.runtime_id.clone(),
runtime_worker_id: worker.runtime_worker_id.clone(),
workdir_id: workdir.workdir_id.clone(),
role: "primary_cwd".to_string(),
linked_at: "4".to_string(),
unlinked_at: None,
};
let projected = merge_worker_registry_projection(None, &worker, vec![link], &[workdir]);
assert_eq!(projected.state, "missing");
let working_directory = projected.working_directory.as_ref().unwrap();
assert_eq!(
working_directory.status,
WorkingDirectoryStatusKind::NotFound
);
assert_eq!(
working_directory.creation_selector.as_deref(),
Some("develop")
);
assert_eq!(working_directory.creation_ref.as_deref(), Some("abcdef"));
assert_eq!(working_directory.current_selector, None);
assert_eq!(working_directory.current_ref.as_deref(), Some("fedcba"));
let occupied_by = working_directory.occupied_by.as_ref().unwrap();
assert_eq!(occupied_by.runtime_id, "embedded");
assert_eq!(occupied_by.runtime_worker_id, 1);
assert_eq!(occupied_by.display_name, "Missing Worker");
assert_eq!(occupied_by.linked_at, "4");
let serialized = serde_json::to_string(&projected).unwrap();
assert!(!serialized.contains("/tmp/"));
assert!(!serialized.contains("materialized_path"));
}
#[tokio::test]
async fn explicit_orchestrator_launch_marks_only_the_dedicated_worker() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let api = test_api(workspace.path()).await;
let workspace_id = api.config.workspace_id.clone();
let Json(generic) = create_workspace_worker(
State(api.clone()),
Json(BrowserCreateWorkerRequest {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
display_name: "Generic Orchestrator Profile Worker".to_string(),
profile: Some("builtin:orchestrator".to_string()),
initial_text: String::new(),
working_directory: None,
}),
)
.await
.unwrap();
assert_eq!(generic.worker.singleton_key, None);
assert!(find_workspace_orchestrator(&api).is_none());
let reserved = create_workspace_worker(
State(api.clone()),
Json(BrowserCreateWorkerRequest {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
display_name: crate::hosts::WORKSPACE_ORCHESTRATOR_SINGLETON_KEY.to_string(),
profile: Some("builtin:orchestrator".to_string()),
initial_text: String::new(),
working_directory: None,
}),
)
.await
.unwrap_err();
assert!(matches!(reserved.error, Error::ReservedWorkerName(_)));
let Json(started) = scoped_start_workspace_orchestrator(
State(api.clone()),
AxumPath(ScopedWorkspacePath {
workspace_id: workspace_id.clone(),
}),
)
.await
.unwrap();
assert_eq!(started.disposition, "created");
let dedicated = started.worker.expect("dedicated Orchestrator Worker");
assert_eq!(
dedicated.singleton_key.as_deref(),
Some(crate::hosts::WORKSPACE_ORCHESTRATOR_SINGLETON_KEY)
);
assert_ne!(dedicated.worker_id, generic.worker_id);
let Json(existing) = scoped_start_workspace_orchestrator(
State(api.clone()),
AxumPath(ScopedWorkspacePath {
workspace_id: workspace_id.clone(),
}),
)
.await
.unwrap();
assert_eq!(existing.disposition, "existing");
assert_eq!(existing.worker.unwrap().worker_id, dedicated.worker_id);
let Json(status) = scoped_workspace_orchestrator_status(
State(api),
AxumPath(ScopedWorkspacePath { workspace_id }),
)
.await
.unwrap();
assert_eq!(status.worker.unwrap().worker_id, dedicated.worker_id);
}
#[tokio::test]
async fn workspace_workdir_summaries_include_runtime_observed_rows() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
api.store
.upsert_workdir_registry(&WorkdirRegistryRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
workdir_id: "managed".to_string(),
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
repository_id: "repo".to_string(),
creation_selector: None,
creation_ref: None,
current_selector: None,
current_ref: None,
materialization_status: "present".to_string(),
cleanliness: "clean".to_string(),
created_at: "1".to_string(),
updated_at: "1".to_string(),
})
.unwrap();
api.store
.upsert_workdir_registry(&WorkdirRegistryRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
workdir_id: "runtime-direct".to_string(),
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
repository_id: "repo".to_string(),
creation_selector: None,
creation_ref: None,
current_selector: None,
current_ref: None,
materialization_status: "present".to_string(),
cleanliness: "unknown".to_string(),
created_at: "1".to_string(),
updated_at: "2".to_string(),
})
.unwrap();
api.store
.upsert_worker_registry(&WorkerRegistryRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
runtime_worker_id: 7,
display_name: "Worker Seven".to_string(),
profile: Some("builtin:coder".to_string()),
retention_state: "normal".to_string(),
transcript_ref: None,
session_ref: None,
summary_ref: None,
diagnostics_ref: None,
created_at: "1".to_string(),
updated_at: "2".to_string(),
})
.unwrap();
api.store
.upsert_worker_workdir_link(&WorkerWorkdirLinkRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
runtime_worker_id: 7,
workdir_id: "managed".to_string(),
role: "primary_cwd".to_string(),
linked_at: "3".to_string(),
unlinked_at: None,
})
.unwrap();
let summaries = working_directory_summaries(&api)
.unwrap_or_else(|err| panic!("working_directory_summaries failed: {}", err.error));
let ids = summaries
.iter()
.map(|summary| summary.working_directory_id.as_str())
.collect::<Vec<_>>();
assert!(ids.contains(&"managed"));
assert!(ids.contains(&"runtime-direct"));
let managed = summaries
.iter()
.find(|summary| summary.working_directory_id == "managed")
.unwrap();
let occupied_by = managed.occupied_by.as_ref().unwrap();
assert_eq!(occupied_by.runtime_id, EMBEDDED_WORKER_RUNTIME_ID);
assert_eq!(occupied_by.runtime_worker_id, 7);
assert_eq!(occupied_by.display_name, "Worker Seven");
assert_eq!(occupied_by.linked_at, "3");
let (runtime_projection, _) =
runtime_working_directory_summaries(&api, EMBEDDED_WORKER_RUNTIME_ID).unwrap_or_else(
|err| panic!("runtime_working_directory_summaries failed: {}", err.error),
);
assert!(
runtime_projection
.iter()
.any(|summary| summary.working_directory_id == "runtime-direct")
);
}
#[test]
fn unmanaged_runtime_workdir_projection_is_typed_and_diagnostic_safe() {
let workdir = WorkdirRegistryRecord {
workspace_id: "workspace-1".to_string(),
workdir_id: "runtime-direct".to_string(),
runtime_id: "embedded".to_string(),
repository_id: "repo".to_string(),
creation_selector: None,
creation_ref: None,
current_selector: None,
current_ref: None,
materialization_status: "present".to_string(),
cleanliness: "unknown".to_string(),
created_at: "1".to_string(),
updated_at: "2".to_string(),
};
let projected = workdir_summary_from_record(&workdir);
assert_eq!(projected.status, WorkingDirectoryStatusKind::Active);
let serialized = serde_json::to_string(&projected).unwrap();
assert!(!serialized.contains("/tmp/"));
assert!(!serialized.contains("materialized_path"));
}
#[test]
fn worker_profile_candidates_are_backend_published_and_mapped() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join(".yoi")).unwrap();
fs::write(
dir.path().join(".yoi/profiles.toml"),
"default = \"builtin:companion\"\n",
)
.unwrap();
let settings = crate::profile_settings::load_profile_settings("workspace-test", dir.path());
for expected in [
"builtin:companion",
"builtin:intake",
"builtin:orchestrator",
"builtin:coder",
"builtin:reviewer",
] {
assert!(
settings
.profiles
.iter()
.any(|profile| profile.profile_id == expected),
"missing {expected}"
);
}
assert!(
settings
.profiles
.iter()
.all(|profile| profile.profile_id != "builtin:default")
);
assert_eq!(
settings.default_profile.as_deref(),
Some("builtin:companion")
);
assert!(matches!(
profile_selector_for_candidate_with_root(dir.path(), "builtin:coder"),
Some(ProfileSelector::Builtin(value)) if value == "builtin:coder"
));
assert!(
profile_selector_for_candidate_with_root(dir.path(), "free-text-profile").is_none()
);
}
#[tokio::test]
async fn worker_launch_options_publish_every_valid_workspace_profile_and_default() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join(".yoi/profiles")).unwrap();
fs::write(
dir.path().join(".yoi/profiles.toml"),
concat!(
"default = \"builtin:companion\"\n",
"[profile.custom]\n",
"path = \".yoi/profiles/custom.dcdl\"\n",
),
)
.unwrap();
fs::write(
dir.path().join(".yoi/profiles/custom.dcdl"),
"slug = \"custom\"; description = \"Custom profile\";\n",
)
.unwrap();
let api = test_app(dir.path()).await;
let response = get_json(api, "/api/workers/launch-options").await;
assert_eq!(response["default_profile"], "builtin:companion");
let profiles = response["profiles"].as_array().unwrap();
for expected in [
"builtin:companion",
"builtin:intake",
"builtin:orchestrator",
"builtin:coder",
"builtin:reviewer",
"project:custom",
] {
assert!(
profiles.iter().any(|profile| profile["id"] == expected),
"missing {expected}: {profiles:?}"
);
}
assert!(
profiles
.iter()
.all(|profile| profile["id"] != "builtin:default")
);
}
#[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 backend_errors_preserve_operation_details() {
let sanitized = sanitize_backend_error(
"failed to open /home/example/.yoi/workspace-backend.local.toml",
);
assert_eq!(
sanitized,
"failed to open /home/example/.yoi/workspace-backend.local.toml"
);
}
#[test]
fn workdir_runtime_miss_uses_exact_typed_code() {
assert_eq!(
workdir_status_from_runtime_miss(&[RuntimeDiagnostic {
code: "working_directory_not_found".to_string(),
severity: DiagnosticSeverity::Warning,
message: "missing".to_string(),
}]),
"not_found"
);
assert_eq!(
workdir_status_from_runtime_miss(&[RuntimeDiagnostic {
code: "some_other_not_found".to_string(),
severity: DiagnosticSeverity::Warning,
message: "not a typed workdir miss".to_string(),
}]),
"unknown"
);
}
#[tokio::test]
async fn profile_settings_api_returns_typed_diagnostics_for_duplicate_selector() {
let dir = tempfile::tempdir().unwrap();
let response = profile_settings_request(
dir.path(),
"POST",
"/settings/profiles",
json!({
"name": "coder",
"content": valid_profile_source("coder"),
"registry_revision": "missing"
}),
)
.await;
assert_eq!(response.0, StatusCode::BAD_REQUEST);
assert_diagnostic(&response.1, "profile_selector_duplicate");
}
#[tokio::test]
async fn profile_settings_api_returns_typed_diagnostics_for_invalid_decodal() {
let dir = tempfile::tempdir().unwrap();
let response = profile_settings_request(
dir.path(),
"POST",
"/settings/profiles",
json!({
"name": "bad",
"content": "not decodal",
"registry_revision": "missing"
}),
)
.await;
assert_eq!(response.0, StatusCode::BAD_REQUEST);
assert!(
diagnostic_codes(&response.1)
.iter()
.any(|code| code.starts_with("profile_source_"))
);
}
#[tokio::test]
async fn profile_settings_api_returns_typed_diagnostic_for_invalid_registry_schema() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join(".yoi")).unwrap();
fs::write(dir.path().join(".yoi/profiles.toml"), "[profile\n").unwrap();
let response = profile_settings_request(
dir.path(),
"POST",
"/settings/profiles",
json!({
"name": "schema",
"content": valid_profile_source("schema"),
"registry_revision": test_file_revision(&dir.path().join(".yoi/profiles.toml"))
}),
)
.await;
assert_eq!(response.0, StatusCode::BAD_REQUEST);
assert_diagnostic(&response.1, "profile_registry_schema_invalid");
}
#[tokio::test]
async fn profile_settings_api_returns_conflict_diagnostic_for_stale_revision() {
let dir = tempfile::tempdir().unwrap();
let response = profile_settings_request(
dir.path(),
"POST",
"/settings/profiles",
json!({
"name": "alpha",
"content": valid_profile_source("alpha"),
"registry_revision": "stale"
}),
)
.await;
assert_eq!(response.0, StatusCode::CONFLICT);
assert_diagnostic(&response.1, "profile_registry_revision_conflict");
}
#[tokio::test]
async fn profile_settings_api_returns_typed_diagnostic_for_too_large_source() {
let dir = tempfile::tempdir().unwrap();
let response = profile_settings_request(
dir.path(),
"POST",
"/settings/profiles",
json!({
"name": "large",
"content": "x".repeat((256 * 1024) + 1),
"registry_revision": "missing"
}),
)
.await;
assert_eq!(response.0, StatusCode::BAD_REQUEST);
assert_diagnostic(&response.1, "profile_source_too_large");
}
#[cfg(unix)]
#[tokio::test]
async fn profile_settings_api_redacts_symlink_escape_response() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join(".yoi/profiles")).unwrap();
fs::write(dir.path().join(".yoi/profiles.toml"), "").unwrap();
let outside = dir.path().join("outside.dcdl");
fs::write(&outside, valid_profile_source("escape")).unwrap();
std::os::unix::fs::symlink(&outside, dir.path().join(".yoi/profiles/escape.dcdl")).unwrap();
let response = profile_settings_request(
dir.path(),
"PUT",
"/settings/profiles/registry",
json!({
"registry_revision": test_file_revision(&dir.path().join(".yoi/profiles.toml")),
"default_profile": null,
"profiles": [{ "name": "escape", "profile_source_id": "project:escape" }]
}),
)
.await;
assert_eq!(response.0, StatusCode::BAD_REQUEST);
assert_diagnostic(&response.1, "profile_source_symlink_escape");
let rendered = response.1.to_string();
assert!(!rendered.contains(dir.path().to_string_lossy().as_ref()));
}
#[tokio::test]
async fn worker_launch_rejects_invalid_project_profile_candidate() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join(".yoi/profiles")).unwrap();
fs::write(
dir.path().join(".yoi/profiles.toml"),
"[profile.bad]\npath = \"profiles/bad.dcdl\"\n",
)
.unwrap();
fs::write(dir.path().join(".yoi/profiles/bad.dcdl"), "not decodal").unwrap();
let candidates =
crate::profile_settings::load_profile_settings("workspace-test", dir.path())
.profiles
.into_iter()
.filter(|profile| {
!profile
.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error)
})
.map(|profile| profile.profile_id)
.collect::<Vec<_>>();
assert!(!candidates.iter().any(|profile| profile == "project:bad"));
assert!(profile_selector_for_candidate_with_root(dir.path(), "project:bad").is_none());
}
fn valid_profile_source(slug: &str) -> String {
format!(
r#"{{
slug = "{slug}";
description = "Test";
scope = "workspace_read";
}}"#
)
}
fn test_file_revision(path: &Path) -> String {
let Ok(metadata) = fs::metadata(path) else {
return "missing".to_string();
};
let modified = metadata
.modified()
.ok()
.and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
.map(|duration| duration.as_nanos())
.unwrap_or_default();
format!("rev:{modified}:{}", metadata.len())
}
async fn profile_settings_request(
workspace_root: &Path,
method: &str,
path: &str,
body: Value,
) -> (StatusCode, Value) {
let app = test_app(workspace_root.to_path_buf()).await;
let request = Request::builder()
.method(method)
.uri(format!("/api/w/{TEST_WORKSPACE_ID}{path}"))
.header(CONTENT_TYPE, "application/json")
.body(Body::from(body.to_string()))
.unwrap();
let response = app.oneshot(request).await.unwrap();
let status = response.status();
let bytes = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
let json = serde_json::from_slice::<Value>(&bytes).unwrap();
(status, json)
}
fn diagnostic_codes(response: &Value) -> Vec<String> {
response["diagnostics"]
.as_array()
.expect("diagnostics array")
.iter()
.map(|diagnostic| diagnostic["code"].as_str().unwrap().to_string())
.collect()
}
fn assert_diagnostic(response: &Value, code: &str) {
let codes = diagnostic_codes(response);
assert!(
!codes.is_empty(),
"diagnostics must not be empty: {response}"
);
assert!(
codes.iter().any(|actual| actual == code),
"missing {code}: {codes:?}"
);
}
struct DeterministicExecutionBackend {
contexts: std::sync::Mutex<
std::collections::HashMap<
worker_runtime::identity::WorkerRef,
worker_runtime::execution::WorkerExecutionContext,
>,
>,
materializer: worker_runtime::working_directory::LocalGitWorktreeMaterializer,
}
impl Default for DeterministicExecutionBackend {
fn default() -> Self {
let unique = format!(
"yoi-deterministic-wd-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
Self {
contexts: std::sync::Mutex::new(std::collections::HashMap::new()),
materializer: worker_runtime::working_directory::LocalGitWorktreeMaterializer::new(
std::env::temp_dir().join(unique),
),
}
}
}
impl worker_runtime::execution::WorkerExecutionBackend for DeterministicExecutionBackend {
fn backend_id(&self) -> &str {
"deterministic-workspace-server-test"
}
fn create_working_directory(
&self,
request: &worker_runtime::catalog::WorkingDirectoryRequest,
) -> std::result::Result<
worker_runtime::catalog::WorkingDirectoryStatus,
worker_runtime::working_directory::WorkingDirectoryDiagnostic,
> {
Ok(self.materializer.create(request)?.status())
}
fn list_working_directories(&self) -> Vec<worker_runtime::catalog::WorkingDirectoryStatus> {
self.materializer
.list_working_directories()
.unwrap_or_default()
}
fn working_directory(
&self,
working_directory_id: &str,
) -> std::result::Result<
worker_runtime::catalog::WorkingDirectoryStatus,
worker_runtime::working_directory::WorkingDirectoryDiagnostic,
> {
self.materializer
.working_directory_status(working_directory_id)
}
fn cleanup_working_directory(
&self,
working_directory_id: &str,
) -> std::result::Result<
worker_runtime::catalog::WorkingDirectoryStatus,
worker_runtime::working_directory::WorkingDirectoryDiagnostic,
> {
self.materializer
.cleanup_working_directory(working_directory_id)
}
fn spawn_worker(
&self,
request: worker_runtime::execution::WorkerExecutionSpawnRequest,
) -> worker_runtime::execution::WorkerExecutionSpawnResult {
let working_directory = match request.request.working_directory.as_ref() {
Some(claim) => match self.materializer.bind_working_directory(
&claim.working_directory_id,
claim.relative_cwd.as_deref(),
) {
Ok(binding) => Some(binding.status()),
Err(diagnostic) => {
return worker_runtime::execution::WorkerExecutionSpawnResult::Rejected(
worker_runtime::execution::WorkerExecutionResult::rejected(
worker_runtime::execution::WorkerExecutionOperation::Spawn,
diagnostic.to_string(),
),
);
}
},
None => None,
};
self.contexts
.lock()
.unwrap()
.insert(request.worker_ref.clone(), request.context);
worker_runtime::execution::WorkerExecutionSpawnResult::Connected {
handle: worker_runtime::execution::WorkerExecutionHandle::new(
request.worker_ref,
self.backend_id(),
),
run_state: worker_runtime::execution::WorkerExecutionRunState::Idle,
working_directory,
}
}
fn dispatch_input(
&self,
handle: &worker_runtime::execution::WorkerExecutionHandle,
input: worker_runtime::interaction::WorkerInput,
) -> worker_runtime::execution::WorkerExecutionResult {
let context = self
.contexts
.lock()
.unwrap()
.get(handle.worker_ref())
.cloned()
.expect("execution context");
let content = input.content.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(25));
let _ = context.publish_protocol_event(protocol::Event::TextDone {
text: format!("server companion echoed: {content}"),
});
});
worker_runtime::execution::WorkerExecutionResult::accepted(
worker_runtime::execution::WorkerExecutionOperation::Input,
worker_runtime::execution::WorkerExecutionRunState::Idle,
)
}
}
fn test_identity() -> WorkspaceIdentity {
WorkspaceIdentity {
workspace_id: TEST_WORKSPACE_ID.to_string(),
display_name: "Test Workspace".to_string(),
created_at: TEST_CREATED_AT.to_string(),
}
}
fn test_server_config(workspace_root: impl Into<PathBuf>) -> ServerConfig {
let workspace_root = workspace_root.into();
let store_root = workspace_root.join(".test-embedded-runtime-store");
let mut config = ServerConfig::local_dev(workspace_root.clone(), test_identity())
.with_embedded_runtime_store_root(store_root);
config.database_path = workspace_root.join(".test-yoi-server.db");
config.runtime_config_path = Some(workspace_root.join(".test-config/runtimes.toml"));
config.repositories = vec![ConfiguredRepository {
id: TEST_REPOSITORY_ID.to_string(),
provider: "git".to_string(),
uri: ".".to_string(),
path: workspace_root,
display_name: Some("Test Repository".to_string()),
default_selector: Some("HEAD".to_string()),
}];
config
}
fn memory_staging_record_json(id: &str, claim: &str) -> String {
json!({
"schema_version": 1,
"id": id,
"extract_run_id": "extract-run-1",
"source": {
"segment_id": "segment-1",
"range": [0, 10],
},
"kind": "working_assumption",
"claim": claim,
"why_useful": "useful for future work",
"staleness": null,
"evidence": [],
"source_refs": [],
})
.to_string()
}
#[tokio::test]
async fn memory_consolidation_backlog_ignores_legacy_filesystem_staging() {
let dir = tempfile::tempdir().unwrap();
let config = test_server_config(dir.path());
let store = SqliteWorkspaceStore::open(&config.database_path).unwrap();
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(store),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
let legacy_staging_dir = dir.path().join(".yoi/memory/_staging");
fs::create_dir_all(&legacy_staging_dir).unwrap();
fs::write(
legacy_staging_dir.join("00000000001J4.json"),
memory_staging_record_json("00000000001J4", "legacy filesystem candidate"),
)
.unwrap();
let output = match start_memory_staging_consolidation(
api,
MemoryConsolidateStagingOperation {
force: true,
threshold_files: None,
threshold_bytes: None,
},
) {
Ok(output) => output,
Err(_) => panic!("unexpected ApiError from memory consolidation trigger"),
};
assert_eq!(output.status, "skipped_empty");
assert_eq!(output.candidate_count, 0);
assert_eq!(output.total_bytes, 0);
}
#[tokio::test]
async fn memory_consolidation_reuses_existing_idle_worker() {
let dir = tempfile::tempdir().unwrap();
let config = test_server_config(dir.path());
let store = SqliteWorkspaceStore::open(&config.database_path).unwrap();
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(store),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
api.authority
.upsert_memory_staging_record(
"00000000000000000000000001",
&memory_staging_record_json("00000000000000000000000001", "first candidate"),
None,
)
.unwrap();
let resolved_config_bundle =
crate::profile_settings::build_workspace_profile_config_bundle(
&api.config.workspace_root,
&api.config.workspace_id,
&api.config.workspace_created_at,
MEMORY_CONSOLIDATION_PROFILE,
)
.unwrap();
let existing = api
.runtime
.spawn_worker(
EMBEDDED_WORKER_RUNTIME_ID,
WorkerSpawnRequest {
requested_worker_name: Some(MEMORY_CONSOLIDATION_PROFILE.to_string()),
intent: WorkerSpawnIntent::WorkspaceOrchestrator,
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 0,
},
profile: ProfileSelector::Builtin(MEMORY_CONSOLIDATION_PROFILE.to_string()),
ticket_assignment: None,
initial_input: None,
working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None,
resolved_config_bundle,
resolved_workspace_api: Some(test_worker_workspace_api(
EMBEDDED_WORKER_RUNTIME_ID,
)),
},
)
.unwrap();
assert_eq!(existing.state, WorkerOperationState::Accepted);
let worker_id = existing.worker.unwrap().worker_id;
let worker = api
.runtime
.worker(EMBEDDED_WORKER_RUNTIME_ID, &worker_id)
.unwrap();
assert_eq!(worker.state, "idle");
assert_eq!(worker.display_name, "Memory Consolidation");
assert_eq!(
worker.singleton_key.as_deref(),
Some(MEMORY_CONSOLIDATION_SINGLETON_KEY)
);
assert!(worker.tags.iter().any(|tag| tag == "memory"));
assert!(worker.tags.iter().any(|tag| tag == "consolidation"));
let second = match start_memory_staging_consolidation(
api.clone(),
MemoryConsolidateStagingOperation {
force: true,
threshold_files: None,
threshold_bytes: None,
},
) {
Ok(output) => output,
Err(_) => panic!("unexpected ApiError from second memory consolidation trigger"),
};
assert_eq!(second.status, "reused");
assert!(second.summary.contains(&worker_id));
let workers_after_second = api
.runtime
.list_workers_for_runtime(EMBEDDED_WORKER_RUNTIME_ID, 20)
.unwrap();
let consolidaters_after_second = workers_after_second
.items
.iter()
.filter(|worker| is_memory_consolidation_worker(worker))
.collect::<Vec<_>>();
assert_eq!(consolidaters_after_second.len(), 1);
assert_eq!(consolidaters_after_second[0].worker_id, worker_id);
}
fn init_clean_git_workspace(path: &std::path::Path) {
for args in [
vec!["init"],
vec!["config", "user.email", "test@example.invalid"],
vec!["config", "user.name", "Yoi Test"],
] {
let status = std::process::Command::new("git")
.arg("-C")
.arg(path)
.args(args)
.status()
.unwrap();
assert!(status.success());
}
std::fs::write(path.join("README.md"), "clean\n").unwrap();
std::fs::write(
path.join(".gitignore"),
".yoi/\n.test-embedded-runtime-store/\n",
)
.unwrap();
for args in [
vec!["add", "README.md", ".gitignore"],
vec!["commit", "-m", "init"],
] {
let status = std::process::Command::new("git")
.arg("-C")
.arg(path)
.args(args)
.status()
.unwrap();
assert!(status.success());
}
}
#[test]
fn worker_source_actor_roles_use_canonical_vocabulary() {
assert_eq!(worker_source_actor_role(true, false), "coder");
assert_eq!(worker_source_actor_role(false, true), "orchestrator");
assert_eq!(worker_source_actor_role(false, false), "worker");
assert_eq!(worker_source_actor_role(true, true), "coder");
}
#[tokio::test]
async fn ticket_assignment_endpoints_read_and_clear_current_assignment() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
let created = browser_ticket_backend(&api)
.unwrap()
.create(ticket::NewTicket::new("Assigned Ticket"))
.unwrap();
let ticket_id = created.id;
let assignment = TicketWorkerAssignmentRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
ticket_id: ticket_id.clone(),
assignment_id: "assignment-api-1".to_string(),
runtime_id: "embedded".to_string(),
worker_id: "42".to_string(),
assigned_by: "test-user".to_string(),
assigned_at: TEST_CREATED_AT.to_string(),
};
api.store
.set_current_ticket_worker_assignment(
&assignment,
None,
"event-api-1",
"operation-api-1",
false,
)
.unwrap();
let path = || ScopedRecordPath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
id: ticket_id.clone(),
};
let Json(read) = scoped_get_ticket_worker_assignment(State(api.clone()), AxumPath(path()))
.await
.unwrap();
assert_eq!(read.assignment, Some(assignment));
let stale = scoped_clear_ticket_worker_assignment(
State(api.clone()),
AxumPath(path()),
Query(ClearTicketWorkerAssignmentQuery {
operation_id: Some("clear-stale".to_string()),
expected_assignment_id: Some("stale-assignment".to_string()),
actor: Some("test-user".to_string()),
}),
)
.await
.unwrap_err()
.into_response();
assert_eq!(stale.status(), StatusCode::CONFLICT);
let Json(cleared) = scoped_clear_ticket_worker_assignment(
State(api.clone()),
AxumPath(path()),
Query(ClearTicketWorkerAssignmentQuery {
operation_id: Some("clear-current".to_string()),
expected_assignment_id: Some("assignment-api-1".to_string()),
actor: Some("test-user".to_string()),
}),
)
.await
.unwrap();
assert_eq!(
cleared.previous_assignment_id.as_deref(),
Some("assignment-api-1")
);
assert_eq!(cleared.assignment, None);
}
#[tokio::test]
async fn authenticated_worker_ticket_mutation_notifies_current_assignment() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
let spawn = |name: &str| WorkerSpawnRequest {
requested_worker_name: Some(name.to_string()),
intent: WorkerSpawnIntent::TicketRole {
ticket_id: name.to_string(),
role: TicketWorkerRole::Coder,
},
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 0,
},
profile: ProfileSelector::Builtin("builtin:coder".to_string()),
ticket_assignment: None,
initial_input: None,
working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None,
resolved_config_bundle: None,
resolved_workspace_api: Some(test_worker_workspace_api(EMBEDDED_WORKER_RUNTIME_ID)),
};
let source_worker = api
.runtime
.spawn_worker(EMBEDDED_WORKER_RUNTIME_ID, spawn("source-worker"))
.unwrap()
.worker
.unwrap();
let recipient_worker = api
.runtime
.spawn_worker(EMBEDDED_WORKER_RUNTIME_ID, spawn("recipient-worker"))
.unwrap()
.worker
.unwrap();
let backend = browser_ticket_backend(&api).unwrap();
let ticket_ref = backend
.create(ticket::NewTicket::new("Notify assigned Worker"))
.unwrap();
api.store
.set_current_ticket_worker_assignment(
&TicketWorkerAssignmentRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
ticket_id: ticket_ref.id.clone(),
assignment_id: "notify-assignment".to_string(),
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
worker_id: recipient_worker.worker_id.clone(),
assigned_by: "test-user".to_string(),
assigned_at: TEST_CREATED_AT.to_string(),
},
None,
"notify-assignment-event",
"notify-assignment-operation",
false,
)
.unwrap();
let mut headers = HeaderMap::new();
headers.insert(
"x-yoi-runtime-id",
axum::http::HeaderValue::from_static(EMBEDDED_WORKER_RUNTIME_ID),
);
headers.insert(
"x-yoi-worker-id",
axum::http::HeaderValue::from_str(&source_worker.worker_id).unwrap(),
);
let response = build_router(api.clone())
.oneshot(
Request::builder()
.method("POST")
.uri(format!(
"/api/w/{TEST_WORKSPACE_ID}/tickets/{}/thread-events",
ticket_ref.id
))
.header("content-type", "application/json")
.header("x-yoi-runtime-id", EMBEDDED_WORKER_RUNTIME_ID)
.header("x-yoi-worker-id", &source_worker.worker_id)
.body(Body::from(
serde_json::to_vec(&NewTicketEvent::new(
TicketEventKind::Comment,
"implementation update",
))
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NO_CONTENT);
let committed = backend.show(ticket_ref.id.clone().into()).unwrap();
let committed_event = committed.events.last().unwrap();
assert_eq!(
committed_event
.attributes
.get("source_runtime_id")
.map(String::as_str),
Some(EMBEDDED_WORKER_RUNTIME_ID)
);
assert_eq!(
committed_event
.attributes
.get("source_worker_id")
.map(String::as_str),
Some(source_worker.worker_id.as_str())
);
assert_eq!(
committed_event
.attributes
.get("source_operation_kind")
.map(String::as_str),
Some("add_event")
);
assert!(committed_event.attributes.contains_key("event_id"));
assert!(committed_event.attributes.contains_key("event_sequence"));
let Json(stale_report) = execute_worker_ticket_test_operation(
State(api.clone()),
AxumPath(ScopedWorkspacePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
}),
headers.clone(),
Json(TicketBackendOperation::AddEvent {
id: ticket_ref.id.clone().into(),
event: NewTicketEvent::new(
TicketEventKind::ImplementationReport,
"non-assigned Worker report",
),
}),
)
.await
.unwrap();
assert!(matches!(stale_report, TicketBackendOperationResult::Unit));
let non_assigned_report = backend.show(ticket_ref.id.clone().into()).unwrap();
assert!(
!non_assigned_report
.events
.last()
.unwrap()
.attributes
.contains_key("source_assignment_id")
);
api.store
.set_current_ticket_worker_assignment(
&TicketWorkerAssignmentRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
ticket_id: ticket_ref.id.clone(),
assignment_id: "source-assignment".to_string(),
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
worker_id: source_worker.worker_id.clone(),
assigned_by: "test-user".to_string(),
assigned_at: TEST_CREATED_AT.to_string(),
},
Some("notify-assignment"),
"source-assignment-event",
"source-assignment-operation",
true,
)
.unwrap();
let _ = execute_worker_ticket_test_operation(
State(api.clone()),
AxumPath(ScopedWorkspacePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
}),
headers.clone(),
Json(TicketBackendOperation::AddEvent {
id: ticket_ref.id.clone().into(),
event: NewTicketEvent::new(
TicketEventKind::ImplementationReport,
"current assignment report",
),
}),
)
.await
.unwrap();
let reported = backend.show(ticket_ref.id.clone().into()).unwrap();
let report_event = reported.events.last().unwrap();
assert_eq!(
report_event
.attributes
.get("source_assignment_id")
.map(String::as_str),
Some("source-assignment")
);
assert_eq!(
report_event
.attributes
.get("source_actor_role")
.map(String::as_str),
Some("coder")
);
let invalid_source = execute_worker_ticket_test_operation(
State(api.clone()),
AxumPath(ScopedWorkspacePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
}),
HeaderMap::new(),
Json(TicketBackendOperation::AddEvent {
id: ticket_ref.id.clone().into(),
event: NewTicketEvent::new(TicketEventKind::Comment, "spoofed update"),
}),
)
.await
.unwrap_err()
.into_response();
assert_eq!(invalid_source.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn queued_ticket_mutation_succeeds_without_orchestrator() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
let source = api
.runtime
.spawn_worker(
EMBEDDED_WORKER_RUNTIME_ID,
WorkerSpawnRequest {
requested_worker_name: Some("orchestrator-source".to_string()),
intent: WorkerSpawnIntent::TicketRole {
ticket_id: "source-ticket".to_string(),
role: TicketWorkerRole::Coder,
},
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 0,
},
profile: ProfileSelector::Builtin("builtin:coder".to_string()),
ticket_assignment: None,
initial_input: None,
working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None,
resolved_config_bundle: None,
resolved_workspace_api: Some(test_worker_workspace_api(
EMBEDDED_WORKER_RUNTIME_ID,
)),
},
)
.unwrap()
.worker
.unwrap();
let backend = browser_ticket_backend(&api).unwrap();
let mut input = ticket::NewTicket::new("Queued notification");
input.workflow_state = Some(TicketWorkflowState::Queued);
let ticket_ref = backend.create(input).unwrap();
let mut headers = HeaderMap::new();
headers.insert(
"x-yoi-runtime-id",
axum::http::HeaderValue::from_static(EMBEDDED_WORKER_RUNTIME_ID),
);
headers.insert(
"x-yoi-worker-id",
axum::http::HeaderValue::from_str(&source.worker_id).unwrap(),
);
let _ = execute_worker_ticket_test_operation(
State(api.clone()),
AxumPath(ScopedWorkspacePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
}),
headers,
Json(TicketBackendOperation::AddEvent {
id: ticket_ref.id.clone().into(),
event: NewTicketEvent::new(TicketEventKind::Comment, "queued update"),
}),
)
.await
.unwrap();
}
#[tokio::test]
async fn orchestrator_running_to_idle_recovers_queued_ticket_without_notification_memory() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
let backend = browser_ticket_backend(&api).unwrap();
let ticket_ref = backend
.create(ticket::NewTicket::new("Recover queued work"))
.unwrap();
backend
.mark_intake_ready(
TicketIdOrSlug::Id(ticket_ref.id.clone()),
ticket::TicketIntakeSummary {
author: Some("intake".to_string()),
body: MarkdownText::new("Ready"),
references: Vec::new(),
},
ticket::TicketStateChange {
from: "planning".to_string(),
to: "ready".to_string(),
reason: "ready".to_string(),
author: Some("intake".to_string()),
body: MarkdownText::new("Ready"),
references: Vec::new(),
},
)
.unwrap();
backend
.queue_ready(TicketIdOrSlug::Id(ticket_ref.id.clone()), "browser-user")
.unwrap();
*api.orchestrator_attention_fingerprint.lock().unwrap() = Some(ticket_ref.id.clone());
let Json(started) = scoped_start_workspace_orchestrator(
State(api.clone()),
AxumPath(ScopedWorkspacePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
}),
)
.await
.unwrap();
assert!(started.online);
assert_eq!(
api.orchestrator_attention_fingerprint
.lock()
.unwrap()
.as_deref(),
Some(ticket_ref.id.as_str())
);
*api.orchestrator_attention_fingerprint.lock().unwrap() = None;
let worker_id = started.worker.as_ref().unwrap().worker_id.clone();
maybe_dispatch_orchestrator_turn_end(
&api,
&worker_id,
Some(protocol::subscription::SubscriptionWorkerState::Idle),
protocol::subscription::SubscriptionWorkerState::Idle,
);
assert!(
api.orchestrator_attention_fingerprint
.lock()
.unwrap()
.is_none()
);
maybe_dispatch_orchestrator_turn_end(
&api,
&worker_id,
Some(protocol::subscription::SubscriptionWorkerState::Running),
protocol::subscription::SubscriptionWorkerState::Idle,
);
assert_eq!(
api.orchestrator_attention_fingerprint
.lock()
.unwrap()
.as_deref(),
Some(ticket_ref.id.as_str())
);
}
#[tokio::test]
async fn worker_spawn_and_restore_assignment_operations_are_idempotent() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
let backend = browser_ticket_backend(&api).unwrap();
let first_ticket = backend
.create(ticket::NewTicket::new("Spawn assignment"))
.unwrap();
let request = WorkerSpawnRequest {
requested_worker_name: Some("assigned-spawn".to_string()),
intent: WorkerSpawnIntent::TicketRole {
ticket_id: first_ticket.id.clone(),
role: TicketWorkerRole::Coder,
},
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 0,
},
profile: ProfileSelector::Builtin("builtin:coder".to_string()),
ticket_assignment: Some(crate::hosts::WorkerTicketAssignmentRequest {
ticket_id: first_ticket.id.clone(),
operation_id: "spawn-assignment-operation".to_string(),
}),
initial_input: None,
working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None,
resolved_config_bundle: None,
resolved_workspace_api: None,
};
let Json(first) = scoped_create_runtime_worker(
State(api.clone()),
AxumPath(ScopedRuntimePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
}),
Json(request.clone()),
)
.await
.unwrap();
let first_worker = first.worker.unwrap();
let Json(projected) = scoped_get_ticket_worker_assignment(
State(api.clone()),
AxumPath(ScopedRecordPath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
id: first_ticket.id.clone(),
}),
)
.await
.unwrap();
assert_eq!(
projected
.worker
.as_ref()
.map(|worker| worker.worker_id.as_str()),
Some(first_worker.worker_id.as_str())
);
let Json(retried) = scoped_create_runtime_worker(
State(api.clone()),
AxumPath(ScopedRuntimePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
}),
Json(request.clone()),
)
.await
.unwrap();
assert_eq!(retried.worker.unwrap().worker_id, first_worker.worker_id);
assert_eq!(
api.store
.list_ticket_worker_assignment_events(TEST_WORKSPACE_ID, &first_ticket.id, 10,)
.unwrap()
.len(),
1
);
let current = api
.store
.get_current_ticket_worker_assignment(TEST_WORKSPACE_ID, &first_ticket.id)
.unwrap()
.unwrap();
api.store
.clear_current_ticket_worker_assignment(
TEST_WORKSPACE_ID,
&first_ticket.id,
Some(&current.assignment_id),
"spawn-unassign-operation",
"spawn-unassign-event",
"test-user",
TEST_CREATED_AT,
)
.unwrap();
api.runtime
.stop_worker(
EMBEDDED_WORKER_RUNTIME_ID,
&first_worker.worker_id,
WorkerLifecycleRequest {
reason: Some("restore assignment test".to_string()),
ticket_assignment: None,
},
)
.unwrap();
let second_ticket = backend
.create(ticket::NewTicket::new("Restore assignment"))
.unwrap();
let _ = scoped_restore_runtime_worker(
State(api.clone()),
AxumPath(ScopedRuntimeWorkerPath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
worker_id: first_worker.worker_id.clone(),
}),
Query(RestoreTicketAssignmentQuery {
ticket_id: Some(second_ticket.id.clone()),
assignment_operation_id: Some("restore-assignment-operation".to_string()),
}),
)
.await
.unwrap();
let Json(retried_restore) = scoped_restore_runtime_worker(
State(api.clone()),
AxumPath(ScopedRuntimeWorkerPath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
worker_id: first_worker.worker_id.clone(),
}),
Query(RestoreTicketAssignmentQuery {
ticket_id: Some(second_ticket.id.clone()),
assignment_operation_id: Some("restore-assignment-operation".to_string()),
}),
)
.await
.unwrap();
assert_eq!(retried_restore.worker_id, first_worker.worker_id);
assert_eq!(retried_restore.result.state, WorkerOperationState::Accepted);
let restored_assignment = api
.store
.get_current_ticket_worker_assignment(TEST_WORKSPACE_ID, &second_ticket.id)
.unwrap()
.unwrap();
assert_eq!(restored_assignment.worker_id, first_worker.worker_id);
api.store
.clear_current_ticket_worker_assignment(
TEST_WORKSPACE_ID,
&second_ticket.id,
Some(&restored_assignment.assignment_id),
"restore-clear-operation",
"restore-clear-event",
"test",
TEST_CREATED_AT,
)
.unwrap();
let mut pending_request = WorkerSpawnRequest {
ticket_assignment: Some(crate::hosts::WorkerTicketAssignmentRequest {
ticket_id: second_ticket.id.clone(),
operation_id: "pending-spawn-operation".to_string(),
}),
..request
};
pending_request.resolved_workspace_api =
Some(test_worker_workspace_api(EMBEDDED_WORKER_RUNTIME_ID));
let (_, pending_fingerprint) = crate::hosts::worker_spawn_idempotency(&pending_request)
.unwrap()
.unwrap();
api.store
.reserve_ticket_assignment_operation(
TEST_WORKSPACE_ID,
"pending-spawn-operation",
&second_ticket.id,
EMBEDDED_WORKER_RUNTIME_ID,
None,
&pending_fingerprint,
TEST_CREATED_AT,
)
.unwrap();
let spawned_before_backend_failure = api
.runtime
.spawn_worker(EMBEDDED_WORKER_RUNTIME_ID, pending_request.clone())
.unwrap()
.worker
.unwrap();
assert!(
api.store
.get_ticket_assignment_operation(TEST_WORKSPACE_ID, "pending-spawn-operation")
.unwrap()
.is_some_and(|operation| operation.worker_id.is_none())
);
let worker_count_before_retry = api.runtime.list_workers(100).items.len();
let Json(reconciled) = scoped_create_runtime_worker(
State(api.clone()),
AxumPath(ScopedRuntimePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
}),
Json(pending_request),
)
.await
.unwrap();
assert_eq!(
reconciled.worker.unwrap().worker_id,
spawned_before_backend_failure.worker_id
);
assert_eq!(
api.runtime.list_workers(100).items.len(),
worker_count_before_retry,
"retrying a reserved lifecycle operation must not spawn another Worker"
);
assert!(
api.store
.get_ticket_assignment_operation(TEST_WORKSPACE_ID, "pending-spawn-operation")
.unwrap()
.and_then(|operation| operation.assignment_id)
.is_some()
);
}
#[tokio::test]
async fn ticket_browser_endpoints_mutate_typed_backend_and_return_thread() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
let ticket_ref = browser_ticket_backend(&api)
.unwrap()
.create(ticket::NewTicket::new("Browser Ticket API"))
.unwrap();
let ticket_id = ticket_ref.id;
let path = || ScopedRecordPath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
id: ticket_id.clone(),
};
let related_ticket_id = browser_ticket_backend(&api)
.unwrap()
.create(ticket::NewTicket::new("Related Browser Ticket"))
.unwrap()
.id;
browser_ticket_backend(&api)
.unwrap()
.add_ticket_relation(
ticket_id.clone().into(),
ticket::NewTicketRelation {
kind: ticket::TicketRelationKind::Related,
target: related_ticket_id.clone(),
note: Some("Browser relation".to_string()),
author: Some("browser-user".to_string()),
},
)
.unwrap();
let Json(edited) = scoped_edit_ticket_item(
State(api.clone()),
AxumPath(path()),
Json(BrowserEditTicketRequest {
title: Some("Browser Ticket API edited".to_string()),
body: Some("Updated from the Browser API.".to_string()),
old_string: None,
new_string: None,
replace_all: false,
target: Some(TicketTargetEdit::Set {
repository_id: "main".to_string(),
ref_selector: Some("feature/api".to_string()),
}),
author: Some("browser-user".to_string()),
}),
)
.await
.unwrap();
assert_eq!(edited.title, "Browser Ticket API edited");
assert_eq!(edited.body, "Updated from the Browser API.");
assert_eq!(edited.repository_id.as_deref(), Some("main"));
assert_eq!(edited.ref_selector.as_deref(), Some("feature/api"));
assert_eq!(edited.assignee, None);
assert_eq!(edited.relations.outgoing.len(), 1);
assert_eq!(edited.relations.outgoing[0].target, related_ticket_id);
assert_eq!(edited.relations.outgoing[0].kind, "related");
let Json(commented) = scoped_append_ticket_event(
State(api.clone()),
AxumPath(path()),
Json(BrowserAppendTicketEventRequest {
role: BrowserTicketThreadRole::Comment,
body: "API comment".to_string(),
author: Some("browser-user".to_string()),
}),
)
.await
.unwrap();
assert!(commented.events.iter().any(|event| {
event.kind == "comment" && event.body.as_deref() == Some("API comment")
}));
let Json(ready) = scoped_transition_ticket_state(
State(api.clone()),
AxumPath(path()),
Json(BrowserTransitionTicketStateRequest {
state: TicketWorkflowState::Ready,
reason: Some("intake complete".to_string()),
body: Some("Ready for queue".to_string()),
author: Some("browser-user".to_string()),
}),
)
.await
.unwrap();
assert_eq!(ready.state, "ready");
let Json(queued) = scoped_queue_ticket(
State(api.clone()),
AxumPath(path()),
Json(BrowserQueueTicketRequest {
queued_by: Some("browser-user".to_string()),
}),
)
.await
.unwrap();
assert_eq!(queued.state, "queued");
assert_eq!(queued.queued_by.as_deref(), Some("browser-user"));
let Json(reviewed) = scoped_review_ticket(
State(api.clone()),
AxumPath(path()),
Json(BrowserReviewTicketRequest {
result: TicketReviewResult::Approve,
body: "API review".to_string(),
author: Some("reviewer".to_string()),
}),
)
.await
.unwrap();
assert!(reviewed.events.iter().any(|event| {
event.kind == "review" && event.body.as_deref() == Some("API review")
}));
let Json(closed) = scoped_close_ticket(
State(api),
AxumPath(path()),
Json(BrowserCloseTicketRequest {
resolution: "Closed through the Browser API.".to_string(),
}),
)
.await
.unwrap();
assert_eq!(closed.state, "closed");
assert_eq!(
closed.resolution.as_deref(),
Some("Closed through the Browser API.")
);
}
#[tokio::test]
async fn ticket_rest_operations_use_workspace_sqlite_backend() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join(".yoi")).unwrap();
fs::write(
dir.path().join(".yoi/workspace.toml"),
format!(
"workspace_id = \"{TEST_WORKSPACE_ID}\"\ncreated_at = \"{TEST_CREATED_AT}\"\ndisplay_name = \"Endpoint Test\"\n\n[ticket]\nlanguage = \"Japanese\"\n\n[ticket.backend]\nprovider = \"builtin:yoi_local\"\nroot = \"server-tickets\"\n"
),
)
.unwrap();
let api = test_api(dir.path()).await;
let ticket_ref = browser_ticket_backend(&api)
.unwrap()
.create(ticket::NewTicket::new("Endpoint configured root"))
.unwrap();
assert!(api.config.database_path.is_file());
assert!(
!dir.path()
.join("server-tickets")
.join(&ticket_ref.id)
.join("item.md")
.exists()
);
assert!(
!dir.path()
.join(".yoi/tickets")
.join(&ticket_ref.id)
.join("item.md")
.exists()
);
}
#[tokio::test]
async fn skills_endpoints_use_workspace_backend_catalog_and_progressive_detail() {
let dir = tempfile::tempdir().unwrap();
let skill_dir = dir.path().join(".yoi/skills/triage-errors");
fs::create_dir_all(&skill_dir).unwrap();
fs::write(
skill_dir.join("SKILL.md"),
"---\nname: triage-errors\ndescription: Use when triaging backend errors and choosing safe diagnostics.\n---\n\n# Triage Errors\n\nInspect logs before changing code.",
)
.unwrap();
let api = test_api(dir.path()).await;
let Json(catalog) = scoped_list_skills(
State(api.clone()),
AxumPath(ScopedWorkspacePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
}),
)
.await
.unwrap_or_else(|error| panic!("skill catalog failed: {}", error.error));
let entry = catalog
.entries
.iter()
.find(|entry| entry.name == "triage-errors")
.expect("workspace Skill catalog entry");
assert_eq!(entry.provenance.id, "workspace:triage-errors");
assert!(
!serde_json::to_string(&catalog)
.unwrap()
.contains("# Triage Errors")
);
let Json(detail) = scoped_get_skill(
State(api),
AxumPath(ScopedSkillPath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
name: "triage-errors".to_string(),
}),
)
.await
.unwrap_or_else(|error| panic!("skill detail failed: {}", error.error));
assert!(detail.body.contains("# Triage Errors"));
assert_eq!(detail.provenance.id, "workspace:triage-errors");
}
async fn test_api(workspace_root: impl Into<PathBuf>) -> WorkspaceApi {
let config = test_server_config(workspace_root);
let store = SqliteWorkspaceStore::open(config.database_path.clone()).unwrap();
WorkspaceApi::new_with_execution_backend(
config,
Arc::new(store),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap()
}
fn seed_cleanup_worker(
api: &WorkspaceApi,
runtime_worker_id: u64,
retention_state: &str,
) -> String {
let now = now_registry_timestamp();
api.store
.upsert_worker_registry(&WorkerRegistryRecord {
workspace_id: api.config.workspace_id.clone(),
runtime_id: "runtime-test".to_string(),
runtime_worker_id,
display_name: runtime_worker_id.to_string(),
profile: None,
retention_state: retention_state.to_string(),
transcript_ref: None,
session_ref: None,
summary_ref: None,
diagnostics_ref: None,
created_at: now.clone(),
updated_at: now,
})
.unwrap();
runtime_worker_id.to_string()
}
fn seed_cleanup_workdir(api: &WorkspaceApi, workdir_id: &str, status: &str, cleanliness: &str) {
let now = now_registry_timestamp();
api.store
.upsert_workdir_registry(&WorkdirRegistryRecord {
workspace_id: api.config.workspace_id.clone(),
workdir_id: workdir_id.to_string(),
runtime_id: "runtime-test".to_string(),
repository_id: "repo-test".to_string(),
creation_selector: Some("HEAD".to_string()),
creation_ref: None,
current_selector: None,
current_ref: None,
materialization_status: status.to_string(),
cleanliness: cleanliness.to_string(),
created_at: now.clone(),
updated_at: now,
})
.unwrap();
}
fn seed_cleanup_link(api: &WorkspaceApi, runtime_worker_id: &str, workdir_id: &str) {
let runtime_worker_id = runtime_worker_id.parse::<u64>().unwrap();
api.store
.upsert_worker_workdir_link(&WorkerWorkdirLinkRecord {
workspace_id: api.config.workspace_id.clone(),
runtime_id: "runtime-test".to_string(),
runtime_worker_id,
workdir_id: workdir_id.to_string(),
role: "primary".to_string(),
linked_at: now_registry_timestamp(),
unlinked_at: None,
})
.unwrap();
}
#[tokio::test]
async fn workspace_scoped_workdir_routes_resolve_runtime_owner_from_registry() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let api = test_api(workspace.path()).await;
seed_cleanup_workdir(&api, "remote-workdir", "present", "clean");
assert_eq!(
registered_workdir_runtime_id(&api, "remote-workdir").unwrap(),
"runtime-test"
);
}
#[tokio::test]
async fn simple_workdir_cleanup_rejects_dirty_and_blocked_candidates() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let api = test_api(workspace.path()).await;
seed_cleanup_workdir(&api, "dirty-workdir", "present", "dirty");
let dirty =
cleanup_working_directory_for_runtime(api.clone(), "runtime-test", "dirty-workdir")
.unwrap_err();
assert!(matches!(
dirty.error,
Error::RuntimeOperationFailed { ref code, .. }
if code == "workspace_cleanup_dirty_confirmation_required"
));
let pinned = seed_cleanup_worker(&api, 17, "pinned");
seed_cleanup_workdir(&api, "blocked-workdir", "present", "clean");
seed_cleanup_link(&api, pinned.as_str(), "blocked-workdir");
let blocked =
cleanup_working_directory_for_runtime(api.clone(), "runtime-test", "blocked-workdir")
.unwrap_err();
assert!(matches!(
blocked.error,
Error::RuntimeOperationFailed { ref code, .. }
if code == "workspace_cleanup_workdir_blocked"
));
}
#[tokio::test]
async fn synthetic_verified_clean_workdir_can_still_use_clean_cleanup_path() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let api = test_api(workspace.path()).await;
seed_cleanup_workdir(&api, "verified-clean", "present", "clean");
let plan = build_runtime_cleanup_plan(&api, "runtime-test")
.unwrap_or_else(|err| panic!("cleanup plan: {}", err.error));
let candidate = plan
.workdirs
.iter()
.find(|candidate| candidate.workdir_id == "verified-clean")
.expect("cleanup candidate");
assert_eq!(candidate.cleanliness, CleanupWorkdirCleanliness::Clean);
assert_eq!(candidate.action, CleanupTargetKind::WorkdirCleanCleanup);
}
#[tokio::test]
async fn cleanup_plan_reports_pinned_running_dirty_not_found_and_redacts_paths() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let api = test_api(workspace.path()).await;
let pinned = seed_cleanup_worker(&api, 1, "pinned");
let unobserved = seed_cleanup_worker(&api, 2, "normal");
seed_cleanup_workdir(&api, "workdir-dirty", "present", "dirty");
seed_cleanup_workdir(&api, "workdir-not-found", "not_found", "clean");
seed_cleanup_link(&api, pinned.as_str(), "workdir-dirty");
seed_cleanup_link(&api, unobserved.as_str(), "workdir-not-found");
let plan = build_runtime_cleanup_plan(&api, "runtime-test")
.unwrap_or_else(|err| panic!("cleanup plan: {}", err.error));
let pinned_worker = plan
.workers
.iter()
.find(|candidate| candidate.worker_id == pinned)
.unwrap();
assert!(pinned_worker.pinned);
assert_eq!(
pinned_worker.blocking_reason.as_deref(),
Some("worker is pinned")
);
let running_linked_workdir = plan
.workdirs
.iter()
.find(|candidate| candidate.workdir_id == "workdir-not-found")
.unwrap();
assert_eq!(
running_linked_workdir.file_status,
CleanupWorkdirFileStatus::NotFound
);
assert_eq!(
running_linked_workdir.action,
CleanupTargetKind::WorkdirRecordDelete
);
assert!(!running_linked_workdir.running_linked);
assert_eq!(running_linked_workdir.blocking_reason.as_deref(), None);
let dirty_workdir = plan
.workdirs
.iter()
.find(|candidate| candidate.workdir_id == "workdir-dirty")
.unwrap();
assert_eq!(dirty_workdir.action, CleanupTargetKind::WorkdirDirtyDiscard);
assert!(dirty_workdir.pinned_linked);
let serialized = serde_json::to_string(&plan).unwrap();
assert!(!serialized.contains("/tmp/secret-runtime-path"));
}
#[tokio::test]
async fn cleanup_execution_rejects_stale_plan_and_pinned_worker_delete() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let api = test_api(workspace.path()).await;
let worker = seed_cleanup_worker(&api, 1, "pinned");
let plan = build_runtime_cleanup_plan(&api, "runtime-test")
.unwrap_or_else(|err| panic!("cleanup plan: {}", err.error));
let target = plan
.workers
.iter()
.find(|candidate| candidate.worker_id == worker)
.unwrap()
.target_id
.clone();
let stale = ExecuteRuntimeCleanupRequest {
expected_plan_revision: "stale".to_string(),
expected_plan_digest: plan.digest.clone(),
worker_target_ids: vec![target.clone()],
workdir_target_ids: Vec::new(),
confirm_dirty_discard_target_ids: Vec::new(),
};
assert!(execute_runtime_cleanup(&api, "runtime-test", stale).is_err());
let pinned = ExecuteRuntimeCleanupRequest {
expected_plan_revision: plan.revision,
expected_plan_digest: plan.digest,
worker_target_ids: vec![target],
workdir_target_ids: Vec::new(),
confirm_dirty_discard_target_ids: Vec::new(),
};
assert!(execute_runtime_cleanup(&api, "runtime-test", pinned).is_err());
}
#[tokio::test]
async fn cleanup_execution_requires_dirty_confirmation_and_deletes_removed_record() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let api = test_api(workspace.path()).await;
seed_cleanup_workdir(&api, "workdir-dirty", "present", "dirty");
seed_cleanup_workdir(&api, "workdir-not-found", "not_found", "clean");
let plan = build_runtime_cleanup_plan(&api, "runtime-test")
.unwrap_or_else(|err| panic!("cleanup plan: {}", err.error));
let dirty_target = plan
.workdirs
.iter()
.find(|candidate| candidate.workdir_id == "workdir-dirty")
.unwrap()
.target_id
.clone();
let removed_target = plan
.workdirs
.iter()
.find(|candidate| candidate.workdir_id == "workdir-not-found")
.unwrap()
.target_id
.clone();
let missing_confirmation = ExecuteRuntimeCleanupRequest {
expected_plan_revision: plan.revision.clone(),
expected_plan_digest: plan.digest.clone(),
worker_target_ids: Vec::new(),
workdir_target_ids: vec![dirty_target],
confirm_dirty_discard_target_ids: Vec::new(),
};
assert!(execute_runtime_cleanup(&api, "runtime-test", missing_confirmation).is_err());
let delete_removed = ExecuteRuntimeCleanupRequest {
expected_plan_revision: plan.revision,
expected_plan_digest: plan.digest,
worker_target_ids: Vec::new(),
workdir_target_ids: vec![removed_target],
confirm_dirty_discard_target_ids: Vec::new(),
};
let response = execute_runtime_cleanup(&api, "runtime-test", delete_removed)
.unwrap_or_else(|err| panic!("cleanup execution: {}", err.error));
assert_eq!(response.results[0].status, "deleted");
assert!(
api.store
.get_workdir_registry(&api.config.workspace_id, "workdir-not-found")
.unwrap()
.is_none()
);
}
async fn test_app(workspace_root: impl Into<PathBuf>) -> Router {
build_router(test_api(workspace_root).await)
}
fn test_profile_archive() -> worker_runtime::profile_archive::ProfileSourceArchive {
use worker_runtime::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveInput};
ProfileSourceArchive::build(ProfileSourceArchiveInput {
id: "profile-source-archive:server-test".to_string(),
entrypoints: std::collections::BTreeMap::from([(
"default".to_string(),
"profiles/default.dcdl".to_string(),
)]),
imports: std::collections::BTreeMap::new(),
sources: std::collections::BTreeMap::from([(
"profiles/default.dcdl".to_string(),
r#"{
slug = "default";
description = "Default";
scope = "workspace_read";
}"#
.to_string(),
)]),
})
.unwrap()
}
fn missing_resource_handle() -> worker_runtime::resource::BackendResourceHandle {
worker_runtime::resource::BackendResourceHandle {
kind: worker_runtime::resource::BackendResourceKind::ProfileSourceArchive,
workspace_id: "workspace-test".to_string(),
scope_id: Some("workspace-profile-source".to_string()),
runtime_id: Some("runtime-test".to_string()),
worker_id: None,
resource_id: "profile-source-archive:missing".to_string(),
digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000"
.to_string(),
operation: worker_runtime::resource::BackendResourceOperation::FetchArchive,
expires_at_unix_seconds: 4_102_444_800,
nonce: "missing-nonce".to_string(),
revision: "missing-revision".to_string(),
generation: None,
max_bytes: worker_runtime::resource::DEFAULT_PROFILE_SOURCE_ARCHIVE_MAX_BYTES,
content_type: worker_runtime::resource::PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE.to_string(),
redaction: worker_runtime::resource::ResourceRedactionPolicy::RuntimeInternalOnly,
audit_correlation_id: "audit-missing".to_string(),
profile_source_graph: Some(
worker_runtime::profile_archive::ProfileSourceGraphSummary {
entrypoints: std::collections::BTreeMap::from([(
"default".to_string(),
"profiles/default.dcdl".to_string(),
)]),
source_count: 1,
import_count: 0,
total_source_bytes: 0,
},
),
}
}
#[tokio::test]
async fn internal_resource_fetch_rest_returns_typed_missing_resource() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let app = test_app(workspace.path()).await;
let handle = missing_resource_handle();
let response = app
.oneshot(
Request::post("/internal/runtime/resources/fetch")
.header("content-type", "application/json")
.body(Body::from(
serde_json::to_vec(
&worker_runtime::resource::BackendResourceFetchRequest {
audit_correlation_id: handle.audit_correlation_id.clone(),
runtime_id: "runtime-test".to_string(),
worker_id: None,
handle,
},
)
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
let error: worker_runtime::resource::BackendResourceError =
serde_json::from_slice(&bytes).unwrap();
assert!(matches!(
error,
worker_runtime::resource::BackendResourceError::MissingResource
));
}
#[tokio::test]
async fn remote_http_resource_fetch_uses_backend_resource_contract() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let api = test_api(workspace.path()).await;
let broker = api.resource_broker.clone();
let archive = test_profile_archive();
let runtime_id = "runtime-test";
let handle = broker.issue_profile_source_archive_handle(
"workspace-test",
Some(runtime_id),
None,
archive,
);
let app = build_router(api);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
let client = worker_runtime::resource::HttpBackendResourceClient::new(
format!("http://{addr}/internal/runtime/resources/fetch"),
None,
);
let response = client
.fetch_resource(worker_runtime::resource::BackendResourceFetchRequest {
audit_correlation_id: handle.audit_correlation_id.clone(),
runtime_id: runtime_id.to_string(),
worker_id: None,
handle: handle.clone(),
})
.await
.expect("remote HTTP resource fetch succeeds");
assert_eq!(response.digest, handle.digest);
let mut tampered = handle;
tampered.scope_id = Some("tampered".to_string());
let error = client
.fetch_resource(worker_runtime::resource::BackendResourceFetchRequest {
audit_correlation_id: tampered.audit_correlation_id.clone(),
runtime_id: runtime_id.to_string(),
worker_id: None,
handle: tampered,
})
.await
.unwrap_err();
assert!(matches!(
error,
worker_runtime::resource::BackendResourceError::Unauthorized { .. }
));
server.abort();
}
fn runtime_test_bundle() -> worker_runtime::config_bundle::ConfigBundle {
worker_runtime::config_bundle::ConfigBundle {
metadata: worker_runtime::config_bundle::ConfigBundleMetadata {
id: "server-test-bundle".to_string(),
digest: String::new(),
revision: "test".to_string(),
workspace_id: "test".to_string(),
created_at: "test".to_string(),
provenance: worker_runtime::config_bundle::ConfigBundleProvenance {
source: "test".to_string(),
detail: None,
},
},
profiles: vec![worker_runtime::config_bundle::ConfigProfileDescriptor {
selector: worker_runtime::catalog::ProfileSelector::Builtin(
"builtin:companion".to_string(),
),
label: Some("server-test".to_string()),
}],
declarations: Vec::new(),
profile_source_archive: None,
profile_source_archive_handle: None,
}
.with_computed_digest()
}
fn runtime_create_request() -> worker_runtime::catalog::CreateWorkerRequest {
let bundle = runtime_test_bundle();
worker_runtime::catalog::CreateWorkerRequest {
idempotency_key: None,
idempotency_fingerprint: None,
profile: worker_runtime::catalog::ProfileSelector::Builtin(
"builtin:companion".to_string(),
),
display_name: None,
profile_source: worker_runtime::catalog::ProfileSourceArchiveSource::Http {
location: worker_runtime::catalog::ProfileSourceArchiveHttpRef {
url: "http://127.0.0.1/profile-source.tar".to_string(),
etag: None,
archive: worker_runtime::profile_archive::ProfileSourceArchiveRef {
id: "test-profile-source".to_string(),
digest: "test-digest".to_string(),
size_bytes: 0,
source_graph: worker_runtime::profile_archive::ProfileSourceGraphSummary {
source_count: 0,
total_source_bytes: 0,
entrypoints: std::collections::BTreeMap::new(),
import_count: 0,
},
},
},
},
config_bundle: Some(worker_runtime::catalog::ConfigBundleRef {
id: bundle.metadata.id,
digest: bundle.metadata.digest,
}),
initial_input: None,
working_directory_request: None,
working_directory: None,
workspace_api: None,
}
}
fn runtime_with_worker() -> (worker_runtime::Runtime, worker_runtime::identity::WorkerRef) {
let runtime = worker_runtime::Runtime::with_execution_backend(
worker_runtime::RuntimeOptions::default(),
Arc::new(DeterministicExecutionBackend::default()),
)
.unwrap();
runtime.store_config_bundle(runtime_test_bundle()).unwrap();
let worker = runtime.create_worker(runtime_create_request()).unwrap();
(runtime, worker.worker_ref)
}
#[tokio::test]
async fn browser_working_directory_create_is_rejected_as_backend_lifecycle() {
let dir = tempfile::tempdir().unwrap();
init_clean_git_workspace(dir.path());
let app = test_app(dir.path()).await;
let workspace_path = format!("/api/w/{TEST_WORKSPACE_ID}/working-directories");
let response = request_json(
app,
"POST",
&workspace_path,
Some(serde_json::json!({
"repository_id": TEST_REPOSITORY_ID,
"selector": "HEAD",
})),
StatusCode::BAD_REQUEST,
)
.await;
assert!(
response["diagnostics"]
.as_array()
.unwrap()
.iter()
.any(|diagnostic| diagnostic["code"] == "backend_workdir_create_unsupported"),
"expected backend lifecycle diagnostic, got {response}"
);
let projected = serde_json::to_string(&response).unwrap();
assert!(!projected.contains(dir.path().to_string_lossy().as_ref()));
}
#[tokio::test]
async fn scoped_worker_create_invalid_relative_cwd_returns_typed_diagnostic() {
let dir = tempfile::tempdir().unwrap();
init_clean_git_workspace(dir.path());
let app = test_app(dir.path()).await;
let working_directory_id = "test-workdir";
let response = request_json(
app,
"POST",
&format!("/api/w/{TEST_WORKSPACE_ID}/workers"),
Some(serde_json::json!({
"runtime_id": "remote-runtime",
"display_name": "Coding Worker",
"profile": "builtin:coder",
"initial_text": "",
"working_directory": {
"working_directory_id": working_directory_id,
"relative_cwd": "../escape"
}
})),
StatusCode::BAD_REQUEST,
)
.await;
assert!(
response["diagnostics"]
.as_array()
.unwrap()
.iter()
.any(|diagnostic| diagnostic["code"] == "working_directory_relative_cwd_invalid"),
"expected typed relative_cwd diagnostic, got {response}"
);
assert!(
response["message"]
.as_str()
.unwrap_or_default()
.contains("working_directory_relative_cwd_invalid")
);
let projected = serde_json::to_string(&response).unwrap();
assert!(!projected.contains(dir.path().to_string_lossy().as_ref()));
}
#[tokio::test]
async fn runtime_connection_settings_add_delete_apply_live_registry() {
let dir = tempfile::tempdir().unwrap();
let app = test_app(dir.path()).await;
let settings = get_json(app.clone(), "/api/settings/runtime-connections").await;
assert_eq!(settings["embedded"]["built_in"], true);
assert_eq!(settings["embedded"]["config_managed"], false);
let added = post_json(
app.clone(),
"/api/settings/runtime-connections/remotes",
serde_json::json!({
"runtime_id": "team-runtime",
"display_name": "Team Runtime",
"endpoint": "https://runtime.example.invalid"
}),
)
.await;
assert_eq!(added["restart_required"], false);
assert_eq!(added["remotes"][0]["runtime_id"], "team-runtime");
assert_eq!(added["remotes"][0]["endpoint_configured"], true);
assert!(
added["diagnostics"]
.as_array()
.unwrap()
.iter()
.any(|diagnostic| diagnostic["code"] == "runtime_registry_applied")
);
let projected = serde_json::to_string(&added).unwrap();
assert!(!projected.contains("runtime.example.invalid"));
let persisted = BackendRuntimesConfigFile::load_from_path(
dir.path().join(".test-config/runtimes.toml"),
)
.unwrap();
assert_eq!(persisted.runtimes.remote.len(), 1);
assert_eq!(persisted.runtimes.remote[0].id, "team-runtime");
assert_eq!(
persisted.runtimes.remote[0].endpoint,
"https://runtime.example.invalid"
);
let launch_options = get_json(app.clone(), "/api/workers/launch-options").await;
let runtimes = launch_options["runtimes"].as_array().unwrap();
let embedded_runtime = runtimes
.iter()
.find(|runtime| runtime["runtime_id"] == EMBEDDED_WORKER_RUNTIME_ID)
.expect("embedded runtime launch option");
assert_eq!(embedded_runtime["working_directory_required"], false);
let team_runtime = runtimes
.iter()
.find(|runtime| runtime["runtime_id"] == "team-runtime")
.expect("team runtime launch option");
assert_eq!(team_runtime["working_directory_required"], true);
let deleted = request_json(
app.clone(),
"DELETE",
"/api/settings/runtime-connections/remotes/team-runtime",
None,
StatusCode::OK,
)
.await;
assert_eq!(deleted["restart_required"], false);
assert_eq!(deleted["remotes"].as_array().unwrap().len(), 0);
let launch_options = get_json(app.clone(), "/api/workers/launch-options").await;
assert!(
!launch_options["runtimes"]
.as_array()
.unwrap()
.iter()
.any(|runtime| runtime["runtime_id"] == "team-runtime")
);
let persisted = BackendRuntimesConfigFile::load_from_path(
dir.path().join(".test-config/runtimes.toml"),
)
.unwrap();
assert!(persisted.runtimes.remote.is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn runtime_connection_delete_rejects_active_remote_workers() {
let (runtime, _worker_ref) = runtime_with_worker();
let runtime_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let runtime_addr = runtime_listener.local_addr().unwrap();
tokio::spawn({
let runtime = runtime.clone();
async move {
worker_runtime::http_server::serve_runtime_http(runtime, runtime_listener, None)
.await
.unwrap()
}
});
let dir = tempfile::tempdir().unwrap();
let app = test_app(dir.path()).await;
let added = post_json(
app.clone(),
"/api/settings/runtime-connections/remotes",
serde_json::json!({
"runtime_id": "busy-runtime",
"display_name": "Busy Runtime",
"endpoint": format!("http://{runtime_addr}")
}),
)
.await;
assert_eq!(added["restart_required"], false);
let workers = get_json(app.clone(), "/api/workers").await;
assert!(
workers["items"]
.as_array()
.unwrap()
.iter()
.any(|worker| worker["runtime_id"] == "busy-runtime"),
"expected remote runtime to report at least one worker, got {workers}"
);
let response = request_json(
app,
"DELETE",
"/api/settings/runtime-connections/remotes/busy-runtime",
None,
StatusCode::CONFLICT,
)
.await;
assert!(
response["message"]
.as_str()
.unwrap()
.contains("remote_runtime_delete_blocked")
);
assert!(
response["diagnostics"]
.as_array()
.unwrap()
.iter()
.any(|diagnostic| { diagnostic["code"] == "remote_runtime_delete_blocked" })
);
let persisted = BackendRuntimesConfigFile::load_from_path(
dir.path().join(".test-config/runtimes.toml"),
)
.unwrap();
assert_eq!(persisted.runtimes.remote.len(), 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn runtime_connection_test_reports_compatible_with_unknown_warnings_without_endpoint_leak()
{
let (runtime, _worker_ref) = runtime_with_worker();
let runtime_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let runtime_addr = runtime_listener.local_addr().unwrap();
tokio::spawn({
let runtime = runtime.clone();
async move {
worker_runtime::http_server::serve_runtime_http(runtime, runtime_listener, None)
.await
.unwrap()
}
});
let dir = tempfile::tempdir().unwrap();
let endpoint = format!("http://{runtime_addr}");
BackendRuntimesConfigFile {
runtimes: WorkspaceBackendRuntimesConfig {
remote: vec![RemoteRuntimeConfigFile {
id: "probe-runtime".to_string(),
endpoint: endpoint.clone(),
display_name: Some("Probe Runtime".to_string()),
token_ref: None,
}],
},
}
.write_to_path(dir.path().join(".test-config/runtimes.toml"))
.unwrap();
let app = test_app(dir.path()).await;
let response = post_json(
app,
"/api/settings/runtime-connections/remotes/probe-runtime/test",
serde_json::json!({}),
)
.await;
assert_eq!(response["state"], "compatible");
let capabilities = response["capabilities"].as_array().unwrap();
assert!(
capabilities
.iter()
.any(|value| value == "runtime.summary:available")
);
assert!(
capabilities
.iter()
.any(|value| value == "workers.list:available")
);
assert!(
capabilities
.iter()
.any(|value| value == "workers.spawn:available")
);
assert!(
response["diagnostics"]
.as_array()
.unwrap()
.iter()
.any(|diagnostic| { diagnostic["code"] == "workers.spawn.available" })
);
let projected = serde_json::to_string(&response).unwrap();
assert!(!projected.contains(&endpoint));
assert!(!projected.contains(&runtime_addr.to_string()));
assert_eq!(response["protocol_version"], serde_json::Value::Null);
}
#[tokio::test(flavor = "multi_thread")]
async fn runtime_connection_test_marks_missing_execution_backend_incompatible() {
let runtime =
worker_runtime::Runtime::with_options(worker_runtime::RuntimeOptions::default());
let runtime_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let runtime_addr = runtime_listener.local_addr().unwrap();
tokio::spawn(async move {
worker_runtime::http_server::serve_runtime_http(runtime, runtime_listener, None)
.await
.unwrap()
});
let dir = tempfile::tempdir().unwrap();
let endpoint = format!("http://{runtime_addr}");
BackendRuntimesConfigFile {
runtimes: WorkspaceBackendRuntimesConfig {
remote: vec![RemoteRuntimeConfigFile {
id: "control-only-runtime".to_string(),
display_name: Some("Control-only Runtime".to_string()),
endpoint,
token_ref: None,
}],
},
}
.write_to_path(dir.path().join(".test-config/runtimes.toml"))
.unwrap();
let app = test_app(dir.path()).await;
let response = post_json(
app,
"/api/settings/runtime-connections/remotes/control-only-runtime/test",
serde_json::json!({}),
)
.await;
assert_eq!(response["state"], "incompatible");
assert!(
response["capabilities"]
.as_array()
.unwrap()
.iter()
.any(|value| { value == "workers.spawn:incompatible" })
);
assert!(
response["diagnostics"]
.as_array()
.unwrap()
.iter()
.any(|diagnostic| {
diagnostic["code"] == "remote_runtime_worker_creation_unavailable"
})
);
}
#[tokio::test]
async fn ticket_rest_search_requires_worker_source_identity_and_rpc_route_is_removed() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
let app = build_router(api);
let response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(format!(
"/api/w/{TEST_WORKSPACE_ID}/tickets/search?state=active"
))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(format!(
"/api/w/{TEST_WORKSPACE_ID}/tickets/search?state=active"
))
.header("x-yoi-runtime-id", "embedded-worker-runtime")
.header("x-yoi-worker-id", "missing-worker")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/api/w/{TEST_WORKSPACE_ID}/tickets/backend"))
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
}
#[tokio::test]
async fn browser_worker_create_uses_workspace_default_and_preserves_unsupported_diagnostics() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join(".yoi")).unwrap();
fs::write(
dir.path().join(".yoi/profiles.toml"),
"default = \"builtin:coder\"\n",
)
.unwrap();
let app = test_app(dir.path()).await;
let created = post_json(
app.clone(),
"/api/workers",
serde_json::json!({
"runtime_id": "embedded-worker-runtime",
"display_name": "",
"initial_text": ""
}),
)
.await;
assert_eq!(created["runtime_id"], "embedded-worker-runtime");
let workers = get_json(app.clone(), "/api/workers").await;
let worker = workers["items"]
.as_array()
.unwrap()
.iter()
.find(|worker| worker["worker_id"] == created["worker_id"])
.expect("created Worker should be listed");
assert_eq!(worker["label"], "Worker");
assert_eq!(worker["profile"], "builtin:coder");
assert!(worker.get("role").is_none());
assert_eq!(worker["worker_id"], created["worker_id"]);
let detail_path = format!(
"/api/runtimes/{}/workers/{}",
created["runtime_id"].as_str().unwrap(),
created["worker_id"].as_str().unwrap()
);
let detail = get_json(app.clone(), detail_path.as_str()).await;
assert_eq!(detail["label"], "Worker");
assert_eq!(detail["worker_id"], created["worker_id"]);
assert!(
created["console_href"]
.as_str()
.unwrap()
.contains("/console")
);
let response = worker_create_not_accepted_error(
"unsupported-runtime".to_string(),
vec![settings_diagnostic(
"remote_runtime_unsupported",
DiagnosticSeverity::Warning,
"Remote Runtime provisioning is unsupported by this v0 worker launch path.",
)],
)
.into_response();
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
let response: Value = serde_json::from_slice(&bytes).unwrap();
let diagnostics = response["diagnostics"].as_array().unwrap();
assert!(diagnostics.len() >= 2);
assert!(
diagnostics
.iter()
.any(|diagnostic| { diagnostic["code"] == "workspace_worker_create_not_accepted" })
);
let projected = serde_json::to_string(&response).unwrap();
assert!(!projected.contains("http://"));
}
#[tokio::test]
async fn browser_worker_create_rejects_non_embedded_no_workdir() {
let dir = tempfile::tempdir().unwrap();
let app = test_app(dir.path()).await;
let response = request_json(
app,
"POST",
"/api/workers",
Some(serde_json::json!({
"runtime_id": "remote-runtime",
"display_name": "Remote Worker",
"profile": "builtin:companion",
"initial_text": ""
})),
StatusCode::BAD_REQUEST,
)
.await;
assert!(
response["message"]
.as_str()
.unwrap_or_default()
.contains("workspace_worker_workdir_required")
);
assert!(
response["diagnostics"]
.as_array()
.unwrap()
.iter()
.any(|diagnostic| { diagnostic["code"] == "workspace_worker_workdir_required" })
);
}
#[tokio::test]
async fn runtime_worker_spawn_rejects_raw_working_directory_fields() {
let dir = tempfile::tempdir().unwrap();
let app = test_app(dir.path()).await;
let response = request_json(
app,
"POST",
"/api/runtimes/embedded-worker-runtime/workers",
Some(serde_json::json!({
"intent": {
"kind": "ticket_role",
"ticket_id": "00001KVZSGT0Q",
"role": "coder"
},
"acceptance": {
"kind": "run_accepted",
"expected_segments": 0
},
"profile": {
"kind": "builtin",
"value": "builtin:coder"
},
"working_directory_request": {
"repository_id": TEST_REPOSITORY_ID,
"local_path": dir.path().display().to_string()
}
})),
StatusCode::UNPROCESSABLE_ENTITY,
)
.await;
assert!(
response["message"]
.as_str()
.unwrap_or_default()
.contains("unknown field"),
"raw working directory field should be rejected: {response}"
);
}
#[tokio::test]
async fn runtime_worker_spawn_rejects_embedded_workdir_request() {
let dir = tempfile::tempdir().unwrap();
let app = test_app(dir.path()).await;
let response = request_json(
app,
"POST",
"/api/runtimes/embedded-worker-runtime/workers",
Some(serde_json::json!({
"intent": {
"kind": "ticket_role",
"ticket_id": "00001KVZSGT0Q",
"role": "coder"
},
"acceptance": {
"kind": "run_accepted",
"expected_segments": 0
},
"profile": {
"kind": "builtin",
"value": "builtin:coder"
},
"working_directory_request": {
"repository_id": TEST_REPOSITORY_ID,
"selector": "HEAD"
}
})),
StatusCode::BAD_REQUEST,
)
.await;
assert!(
response["diagnostics"]
.as_array()
.unwrap()
.iter()
.any(|diagnostic| diagnostic["code"] == "embedded_worker_workdir_unsupported"),
"expected embedded workdir diagnostic, got {response}"
);
let projected = serde_json::to_string(&response).unwrap();
assert!(!projected.contains(dir.path().to_string_lossy().as_ref()));
}
#[tokio::test]
async fn browser_worker_create_rejects_extra_request_fields() {
let dir = tempfile::tempdir().unwrap();
let app = test_app(dir.path()).await;
let response = request_json(
app,
"POST",
"/api/workers",
Some(serde_json::json!({
"runtime_id": "embedded-worker-runtime",
"display_name": "Coding Worker",
"profile": "builtin:coder",
"initial_text": "",
"kind": "internal"
})),
StatusCode::UNPROCESSABLE_ENTITY,
)
.await;
assert!(
response["message"]
.as_str()
.unwrap_or_default()
.contains("unknown field")
);
}
#[tokio::test]
async fn serves_bounded_read_apis_and_static_spa_separately() {
let dir = tempfile::tempdir().unwrap();
let static_dir = dir.path().join("static");
std::fs::create_dir_all(static_dir.join("assets")).unwrap();
std::fs::write(static_dir.join("index.html"), "<main>Yoi Workspace</main>").unwrap();
std::fs::write(static_dir.join("assets/app.js"), "console.log('yoi');").unwrap();
let store = SqliteWorkspaceStore::in_memory().unwrap();
let mut config = test_server_config(dir.path());
write_ticket(
&config.database_path,
TEST_WORKSPACE_ID,
"API Ticket",
ticket::TicketWorkflowState::Ready,
);
let sqlite_store = SqliteWorkspaceStore::open(&config.database_path).unwrap();
sqlite_store
.upsert_workspace(&WorkspaceRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
owner_account_id: None,
display_name: "Test Workspace".to_string(),
state: "active".to_string(),
created_at: "2026-01-01T00:00:00Z".to_string(),
updated_at: "2026-01-01T00:00:00Z".to_string(),
})
.await
.unwrap();
sqlite_store
.upsert_objective(&ObjectiveRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
objective_id: "00000000001J3".to_string(),
title: "API Objective".to_string(),
state: "active".to_string(),
body_md: "Objective body.\n".to_string(),
created_at: "2026-01-01T00:00:00Z".to_string(),
updated_at: "2026-01-02T00:00:00Z".to_string(),
})
.unwrap();
sqlite_store
.replace_objective_ticket_links(
TEST_WORKSPACE_ID,
"00000000001J3",
&[ObjectiveTicketLinkRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
objective_id: "00000000001J3".to_string(),
ticket_id: "00000000001J2".to_string(),
kind: "linked".to_string(),
created_at: "2026-01-01T00:00:00Z".to_string(),
}],
)
.unwrap();
sqlite_store
.upsert_objective_resource(&ObjectiveResourceRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
objective_id: "00000000001J3".to_string(),
resource_path: "memory-architecture-overview.md".to_string(),
body: "# Memory architecture\n\nResource body.\n".to_string(),
media_type: Some("text/markdown".to_string()),
created_at: "2026-01-01T00:00:00Z".to_string(),
updated_at: "2026-01-02T00:00:00Z".to_string(),
})
.unwrap();
sqlite_store
.upsert_memory_document(&MemoryDocumentRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
body_md: "# Memory\n\n## Project facts\n\n- Frontend can read this document.\n"
.to_string(),
created_at: "2026-01-01T00:00:00Z".to_string(),
updated_at: "2026-01-02T00:00:00Z".to_string(),
})
.unwrap();
sqlite_store
.upsert_memory_staging_record(&MemoryStagingRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
candidate_id: "00000000001J4".to_string(),
raw_json: memory_staging_record_json("00000000001J4", "SQLite memory candidate"),
source_path: None,
imported_at: "2026-01-01T00:00:00Z".to_string(),
})
.unwrap();
assert_eq!(
sqlite_store
.count_memory_staging_records(TEST_WORKSPACE_ID)
.unwrap(),
1
);
write_objective(
dir.path(),
"00000000001J5",
"Filesystem Only Objective",
"active",
);
config.static_assets_dir = Some(static_dir);
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(store),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
let app = build_router(api);
let workspace = get_json(app.clone(), "/api/workspace").await;
assert_eq!(workspace["workspace_id"], TEST_WORKSPACE_ID);
assert_eq!(workspace["display_name"], "Test Workspace");
assert_eq!(workspace["record_authority"], "local_yoi_project_records");
assert_eq!(
workspace["extension_points"]["host_worker_bridge"]["status"],
"runtime_registry"
);
let workspace_companion = &workspace["extension_points"]["companion_console"];
assert_ne!(workspace_companion["status"], "not_connected");
assert!(
!workspace_companion["note"]
.as_str()
.unwrap()
.contains("browser input remains disabled"),
"stale Companion Console note returned: {workspace_companion}"
);
if workspace_companion["status"] == "not_input_capable" {
assert!(
!workspace_companion["diagnostics"]
.as_array()
.unwrap()
.is_empty(),
"not_input_capable workspace companion_console lacks typed diagnostics: {workspace_companion}"
);
}
let scoped_workspace = get_json(
app.clone(),
&format!("/api/w/{TEST_WORKSPACE_ID}/workspace"),
)
.await;
assert_eq!(scoped_workspace["workspace_id"], TEST_WORKSPACE_ID);
let mismatched_workspace = request_json(
app.clone(),
"GET",
"/api/w/not-this-workspace/workspace",
None,
StatusCode::NOT_FOUND,
)
.await;
assert_eq!(
mismatched_workspace["diagnostics"][0]["code"],
"workspace_id_mismatch"
);
assert!(
!mismatched_workspace
.to_string()
.contains(dir.path().to_string_lossy().as_ref())
);
let unscoped_objectives = app
.clone()
.oneshot(
Request::builder()
.uri("/objectives?focus=active")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(unscoped_objectives.status(), StatusCode::TEMPORARY_REDIRECT);
let expected_location = format!("/w/{TEST_WORKSPACE_ID}/objectives?focus=active");
assert_eq!(
unscoped_objectives
.headers()
.get(LOCATION)
.and_then(|value| value.to_str().ok()),
Some(expected_location.as_str())
);
let tickets = get_json(app.clone(), "/api/tickets").await;
assert_eq!(tickets["items"][0]["title"], "API Ticket");
assert_eq!(tickets["items"][0]["state"], "ready");
let objectives = get_json(app.clone(), "/api/objectives").await;
assert_eq!(objectives["items"].as_array().unwrap().len(), 1);
assert_eq!(objectives["items"][0]["id"], "00000000001J3");
assert_eq!(objectives["items"][0]["summary"], "Objective body.");
assert_eq!(objectives["record_authority"], "workspace-sqlite");
let scoped_objectives = get_json(
app.clone(),
&format!("/api/w/{TEST_WORKSPACE_ID}/objectives"),
)
.await;
assert_eq!(scoped_objectives["items"][0]["id"], "00000000001J3");
let limited_objectives = get_json(
app.clone(),
&format!("/api/w/{TEST_WORKSPACE_ID}/objectives?limit=0"),
)
.await;
assert_eq!(limited_objectives["items"].as_array().unwrap().len(), 0);
let scoped_objective = get_json(
app.clone(),
&format!("/api/w/{TEST_WORKSPACE_ID}/objectives/00000000001J3"),
)
.await;
assert_eq!(scoped_objective["id"], "00000000001J3");
assert_eq!(scoped_objective["record_source"], "workspace-sqlite");
assert_eq!(
scoped_objective["resources"][0]["path"],
"memory-architecture-overview.md"
);
let memory_document =
get_json(app.clone(), &format!("/api/w/{TEST_WORKSPACE_ID}/memory")).await;
assert_eq!(memory_document["created_at"], "2026-01-01T00:00:00Z");
assert_eq!(memory_document["updated_at"], "2026-01-02T00:00:00Z");
assert_eq!(memory_document["bytes"], 63);
assert_eq!(memory_document["record_source"], "workspace-sqlite");
assert!(
memory_document["body_md"]
.as_str()
.unwrap()
.contains("Frontend can read this document.")
);
let memory_staging = get_json(
app.clone(),
&format!("/api/w/{TEST_WORKSPACE_ID}/memory/staging?limit=10"),
)
.await;
assert_eq!(
memory_staging["record_authority"],
"sqlite_workspace_authority.memory_staging"
);
assert_eq!(memory_staging["total_valid_count"], 1);
assert_eq!(memory_staging["invalid_count"], 0);
assert_eq!(
memory_staging["items"][0]["record"]["claim"],
"SQLite memory candidate"
);
let repositories = get_json(app.clone(), "/api/repositories").await;
assert_eq!(repositories["items"][0]["id"], TEST_REPOSITORY_ID);
assert_eq!(repositories["items"][0]["kind"], "git");
assert_eq!(
repositories["items"][0]["record_authority"],
"workspace-control-plane"
);
assert!(
repositories
.to_string()
.contains("repository_git_unavailable")
);
let repository_detail = get_json(app.clone(), "/api/repositories/main").await;
assert_eq!(repository_detail["item"]["id"], TEST_REPOSITORY_ID);
let scoped_repository_detail = get_json(
app.clone(),
&format!("/api/w/{TEST_WORKSPACE_ID}/repositories/main"),
)
.await;
assert_eq!(scoped_repository_detail["item"]["id"], TEST_REPOSITORY_ID);
let repository_log = get_json(app.clone(), "/api/repositories/main/log?limit=3").await;
assert_eq!(repository_log["repository_id"], TEST_REPOSITORY_ID);
assert_eq!(repository_log["default_selector"], "HEAD");
assert_eq!(repository_log["limit"], 3);
let removed_repository_tickets = app
.clone()
.oneshot(
Request::builder()
.uri("/api/repositories/main/tickets")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(removed_repository_tickets.status(), StatusCode::NOT_FOUND);
let unknown_repository_response = app
.clone()
.oneshot(
Request::builder()
.uri("/api/repositories/nope")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(unknown_repository_response.status(), StatusCode::NOT_FOUND);
let hosts = get_json(app.clone(), "/api/hosts").await;
assert_eq!(hosts["source"], "worker_runtime_registry");
assert_eq!(hosts["items"][0]["runtime_id"], "embedded-worker-runtime");
let host_id = hosts["items"][0]["host_id"].as_str().unwrap().to_string();
assert_eq!(hosts["items"][0]["kind"], "embedded-worker-runtime-host");
assert_eq!(
hosts["items"][0]["capabilities"]["workspace_scope"],
"backend_internal"
);
assert!(!hosts.to_string().contains("metadata.json"));
let runtimes = get_json(app.clone(), "/api/runtimes").await;
assert_eq!(runtimes["source"], "worker_runtime_registry");
assert_eq!(
runtimes["items"][0]["runtime_id"],
"embedded-worker-runtime"
);
assert_eq!(
runtimes["items"][0]["source"]["kind"],
"embedded_worker_runtime"
);
assert_eq!(
runtimes["items"][0]["source"]["identity_authority"],
"runtime_registry_projection"
);
assert!(!runtimes.to_string().contains("/workspace/demo"));
assert_eq!(runtimes["items"][0]["host_ids"][0], host_id);
let workers = get_json(app.clone(), "/api/workers").await;
let worker_items = workers["items"].as_array().unwrap();
assert!(
worker_items
.iter()
.all(|worker| worker["role"] != "builtin:companion"),
"companion auto-start should not create runtime workers: {workers}"
);
let companion_status = get_json(app.clone(), "/api/companion/status").await;
assert_eq!(companion_status["state"], "disabled");
assert!(companion_status["worker"].is_null());
assert_eq!(companion_status["transport"]["kind"], "none");
assert_eq!(companion_status["transport"]["completion"], "disabled");
assert!(!companion_status.to_string().contains("/workspace/demo"));
let companion_message = post_json(
app.clone(),
"/api/companion/messages",
json!({ "content": "hello companion" }),
)
.await;
assert_eq!(companion_message["state"], "rejected");
assert_eq!(
companion_message["diagnostics"][0]["code"],
"companion_disabled"
);
assert!(companion_message["user_item"].is_null());
assert!(companion_message["assistant_item"].is_null());
assert!(!companion_message.to_string().contains("/workspace/demo"));
let companion_transcript = get_json(app.clone(), "/api/companion/transcript").await;
assert_eq!(companion_transcript["total_items"], 0);
let host_workers = get_json(app.clone(), &format!("/api/hosts/{host_id}/workers")).await;
assert!(
host_workers["items"]
.as_array()
.unwrap()
.iter()
.all(|worker| worker["role"] != "builtin:companion")
);
let runs_response = app
.clone()
.oneshot(
Request::builder()
.uri("/api/runs")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(runs_response.status(), StatusCode::NOT_FOUND);
let runners_response = app
.clone()
.oneshot(
Request::builder()
.uri("/api/runners")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(runners_response.status(), StatusCode::NOT_FOUND);
let static_response = app
.clone()
.oneshot(
Request::builder()
.uri("/assets/app.js")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(static_response.status(), StatusCode::OK);
assert_eq!(
static_response.headers().get(CONTENT_TYPE).unwrap(),
"text/javascript; charset=utf-8"
);
let spa_response = app
.clone()
.oneshot(
Request::builder()
.uri("/tickets/00000000001J2")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(spa_response.status(), StatusCode::OK);
let bytes = to_bytes(spa_response.into_body(), usize::MAX)
.await
.unwrap();
assert!(
String::from_utf8(bytes.to_vec())
.unwrap()
.contains("Yoi Workspace")
);
let api_miss = app
.oneshot(
Request::builder()
.uri("/api/nope")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(api_miss.status(), StatusCode::NOT_FOUND);
let bytes = to_bytes(api_miss.into_body(), usize::MAX).await.unwrap();
assert!(
!String::from_utf8(bytes.to_vec())
.unwrap()
.contains("Yoi Workspace")
);
}
#[tokio::test]
async fn companion_routes_report_disabled_without_spawning_worker() {
let temp = tempfile::tempdir().unwrap();
let config = test_server_config(temp.path().join("workspace"));
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(SqliteWorkspaceStore::in_memory().unwrap()),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
let app = build_router(api);
let workspace = get_json(app.clone(), "/api/workspace").await;
let workspace_companion = &workspace["extension_points"]["companion_console"];
assert_eq!(workspace_companion["status"], "disabled");
assert_eq!(
workspace_companion["diagnostics"][0]["code"],
"companion_disabled"
);
assert!(
workspace_companion["note"]
.as_str()
.unwrap()
.contains("auto-start has been removed")
);
let status = get_json(app.clone(), "/api/companion/status").await;
assert_eq!(status["state"], "disabled");
assert_eq!(status["transport"]["completion"], "disabled");
assert!(status["worker"].is_null());
let response = post_json(
app.clone(),
"/api/companion/messages",
serde_json::json!({ "content": "from legacy route" }),
)
.await;
assert_eq!(response["state"], "rejected");
assert_eq!(response["diagnostics"][0]["code"], "companion_disabled");
assert!(response["user_item"].is_null());
assert!(response["assistant_item"].is_null());
let transcript = get_json(app.clone(), "/api/companion/transcript").await;
assert_eq!(transcript["state"], "disabled");
assert_eq!(transcript["total_items"], 0);
let workers = get_json(app, "/api/workers").await;
assert!(
workers["items"]
.as_array()
.unwrap()
.iter()
.all(|worker| worker["role"] != "builtin:companion"),
"disabled companion route should not spawn workers: {workers}"
);
}
#[tokio::test]
async fn embedded_runtime_fs_store_restores_catalog_and_stops_failed_execution() {
let dir = tempfile::tempdir().unwrap();
let config = test_server_config(dir.path().join("workspace"));
let store_root = config.embedded_runtime_store_root.clone();
let bundle = runtime_test_bundle();
let bundle_id = bundle.metadata.id.clone();
let api = WorkspaceApi::new_with_execution_backend(
config.clone(),
Arc::new(SqliteWorkspaceStore::in_memory().unwrap()),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.expect("fs-backed api starts");
let synced = api
.runtime
.sync_config_bundle("embedded-worker-runtime", bundle)
.expect("sync config bundle");
assert_eq!(synced.state, WorkerOperationState::Accepted);
assert!(store_root.exists(), "fs-store root should be created");
let spawned = api
.runtime
.spawn_worker(
"embedded-worker-runtime",
WorkerSpawnRequest {
intent: WorkerSpawnIntent::TicketRole {
ticket_id: "00001KVZSGT0Q".to_string(),
role: TicketWorkerRole::Coder,
},
requested_worker_name: None,
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 0,
},
profile: ProfileSelector::Builtin("builtin:coder".to_string()),
ticket_assignment: None,
initial_input: None,
working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None,
resolved_config_bundle: None,
resolved_workspace_api: Some(test_worker_workspace_api(
"embedded-worker-runtime",
)),
},
)
.expect("spawn worker");
assert_eq!(spawned.state, WorkerOperationState::Accepted);
let worker_id = spawned.worker.expect("created worker").worker_id;
let sent = api
.runtime
.send_input(
"embedded-worker-runtime",
&worker_id,
WorkerInputRequest {
kind: WorkerInputKind::User,
content: "persist me".to_string(),
segments: None,
},
)
.expect("send input");
assert_eq!(sent.state, WorkerOperationState::Accepted);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
loop {
let detail = api
.runtime
.worker("embedded-worker-runtime", &worker_id)
.expect("worker detail");
if detail.state == "idle" {
break;
}
assert!(
std::time::Instant::now() < deadline,
"timed out waiting for deterministic worker completion"
);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
drop(api);
let restored = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(SqliteWorkspaceStore::in_memory().unwrap()),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.expect("restored fs-backed api starts");
let restored_worker = restored
.runtime
.worker("embedded-worker-runtime", &worker_id)
.expect("restored worker");
assert_eq!(restored_worker.state, "stopped");
assert!(!restored_worker.capabilities.can_stop);
let bundles = restored
.runtime
.list_config_bundles("embedded-worker-runtime")
.expect("config bundle list");
assert!(
bundles
.bundles
.iter()
.any(|summary| summary.id == bundle_id)
);
let rejected_input = restored
.runtime
.send_input(
"embedded-worker-runtime",
&worker_id,
WorkerInputRequest {
kind: WorkerInputKind::User,
content: "should not be routed to corrupted handle".to_string(),
segments: None,
},
)
.expect("stale worker input is projected as an operation result");
assert_eq!(rejected_input.state, WorkerOperationState::Rejected);
}
#[tokio::test]
async fn embedded_runtime_store_root_is_isolated_and_not_exposed_by_browser_api() {
let dir = tempfile::tempdir().unwrap();
let data_dir = dir.path().join("user-data");
let workspace_root = dir.path().join("workspace");
let default_root =
ServerConfig::embedded_runtime_store_root_for_data_dir(&data_dir, TEST_WORKSPACE_ID);
assert_eq!(
default_root,
data_dir
.join("server")
.join("workspaces")
.join(TEST_WORKSPACE_ID)
.join("embedded-runtime")
);
assert!(!default_root.starts_with(workspace_root.join(".yoi")));
let config = ServerConfig::local_dev(workspace_root, test_identity())
.with_embedded_runtime_store_root(default_root.clone());
let app = build_router(
WorkspaceApi::new_with_execution_backend(
config,
Arc::new(SqliteWorkspaceStore::in_memory().unwrap()),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap(),
);
let raw_store_root = default_root.to_string_lossy().to_string();
for uri in [
"/api/workspace",
"/api/hosts",
"/api/runtimes",
"/api/workers",
] {
let body = get_json(app.clone(), uri).await;
let serialized = serde_json::to_string(&body).unwrap();
assert!(
!serialized.contains(&raw_store_root),
"{uri} leaked embedded runtime store root: {serialized}"
);
}
}
#[tokio::test]
async fn empty_repository_config_returns_empty_list_with_warning() {
let root = tempfile::tempdir().unwrap();
let mut config = test_server_config(root.path());
config.repositories.clear();
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(SqliteWorkspaceStore::in_memory().unwrap()),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
let app = build_router(api);
let repositories = get_json(app, "/api/repositories").await;
assert!(repositories["items"].as_array().unwrap().is_empty());
assert_eq!(
repositories["diagnostics"][0]["code"],
"repository_config_empty"
);
}
#[tokio::test]
async fn repository_log_rejects_unknown_or_unsupported_configured_repository() {
let root = tempfile::tempdir().unwrap();
let mut config = test_server_config(root.path());
config.repositories = vec![ConfiguredRepository {
id: "files".to_string(),
provider: "local_fs".to_string(),
uri: ".".to_string(),
path: root.path().to_path_buf(),
display_name: None,
default_selector: None,
}];
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(SqliteWorkspaceStore::in_memory().unwrap()),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
let app = build_router(api);
let unknown = request_json(
app.clone(),
"GET",
"/api/repositories/main/log",
None,
StatusCode::NOT_FOUND,
)
.await;
assert_eq!(
unknown["diagnostics"][0]["code"],
"repository_not_configured"
);
let unsupported = request_json(
app,
"GET",
"/api/repositories/files/log",
None,
StatusCode::BAD_REQUEST,
)
.await;
assert_eq!(
unsupported["diagnostics"][0]["code"],
"repository_provider_unsupported"
);
}
#[tokio::test]
async fn embedded_runtime_api_routes_by_runtime_and_worker_ids_without_leaking_internals() {
let dir = tempfile::tempdir().unwrap();
let store = SqliteWorkspaceStore::in_memory().unwrap();
let config = test_server_config(dir.path());
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(store),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
let app = build_router(api);
let runtimes = get_json(app.clone(), "/api/runtimes").await;
let embedded_summary = runtimes["items"]
.as_array()
.unwrap()
.iter()
.find(|runtime| runtime["runtime_id"] == "embedded-worker-runtime")
.expect("embedded runtime summary");
assert_eq!(
embedded_summary["source"]["kind"],
"embedded_worker_runtime"
);
assert_eq!(embedded_summary["source"]["status"], "active");
assert_eq!(
embedded_summary["capabilities"]["workspace_scope"],
"backend_internal"
);
assert_eq!(embedded_summary["capabilities"]["has_workspace_fs"], false);
let spawned = post_json(
app.clone(),
"/api/runtimes/embedded-worker-runtime/workers",
json!({
"intent": {
"kind": "ticket_role",
"ticket_id": "00001KVZSGT0Q",
"role": "coder"
},
"requested_worker_name": "api-friendly-name",
"acceptance": {
"kind": "run_accepted",
"expected_segments": 0
},
"profile": {
"kind": "builtin",
"value": "builtin:coder"
}
}),
)
.await;
assert_eq!(spawned["state"], "accepted");
let diagnostics = spawned["diagnostics"].as_array().unwrap();
assert!(diagnostics.iter().all(|diagnostic| {
!diagnostic["message"]
.as_str()
.unwrap_or_default()
.contains("/workspace/demo")
}));
let worker_id = spawned["worker"]["worker_id"].as_str().unwrap().to_string();
assert_eq!(spawned["worker"]["runtime_id"], "embedded-worker-runtime");
assert_eq!(
spawned["worker"]["workspace"]["visibility"],
"backend_internal"
);
assert_eq!(
spawned["worker"]["implementation"]["kind"],
"embedded_worker_runtime"
);
let worker = get_json(
app.clone(),
&format!("/api/runtimes/embedded-worker-runtime/workers/{worker_id}"),
)
.await;
assert_eq!(worker["worker_id"], worker_id);
assert_eq!(worker["runtime_id"], "embedded-worker-runtime");
let stopped_workers = get_json(
app.clone(),
"/api/runtimes/embedded-worker-runtime/workers?status=stopped",
)
.await;
assert!(stopped_workers["items"].as_array().unwrap().is_empty());
let restored = post_json(
app.clone(),
&format!("/api/runtimes/embedded-worker-runtime/workers/{worker_id}/restore"),
json!({}),
)
.await;
assert_eq!(restored["result"]["state"], "accepted");
assert_eq!(restored["result"]["worker"]["worker_id"], worker_id);
let accepted = post_json(
app.clone(),
&format!("/api/runtimes/embedded-worker-runtime/workers/{worker_id}/input"),
json!({
"kind": "user",
"content": "hello from browser-facing api"
}),
)
.await;
assert_eq!(accepted["state"], "accepted");
assert_eq!(accepted["runtime_id"], "embedded-worker-runtime");
assert_eq!(accepted["worker_id"], worker_id);
assert!(accepted["diagnostics"].as_array().unwrap().is_empty());
let transcript_route = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(format!(
"/api/runtimes/embedded-worker-runtime/workers/{worker_id}/transcript?start=0&limit=10"
))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(transcript_route.status(), StatusCode::NOT_FOUND);
let wrong_runtime = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(format!(
"/api/runtimes/unknown-runtime/workers/{worker_id}/input"
))
.header(CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&json!({
"kind": "user",
"content": "wrong runtime"
}))
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(wrong_runtime.status(), StatusCode::NOT_FOUND);
let projected = format!(
"{}{}{}{}{}{}",
embedded_summary, spawned, worker, stopped_workers, restored, accepted
);
for forbidden in [
dir.path().to_string_lossy().as_ref(),
"metadata.json",
"socket",
"session",
"token",
"credential",
"provider",
] {
assert!(
!projected.contains(forbidden),
"embedded api projection leaked forbidden term: {forbidden}: {projected}"
);
}
}
#[tokio::test]
async fn proxies_worker_protocol_ws_as_raw_events() {
let (runtime, worker_ref, endpoint) = spawn_runtime_worker().await;
let source = RuntimeObservationSourceConfig {
runtime_id: "runtime-a".into(),
worker_id: "worker-a".into(),
endpoint,
bearer_token: None,
};
let (url, _dir) = spawn_workspace_proxy(source).await;
let (mut stream, _) = connect_async(&url).await.unwrap();
assert!(matches!(
next_client_frame(&mut stream).await,
protocol::Event::Snapshot { .. }
));
stream
.send(Message::Text(
serde_json::to_string(&protocol::Method::ListCompletions {
kind: protocol::CompletionKind::File,
prefix: String::new(),
})
.unwrap()
.into(),
))
.await
.unwrap();
assert!(matches!(
next_client_frame(&mut stream).await,
protocol::Event::Completions { .. }
));
runtime
.observe_worker_event(
&worker_ref,
protocol::Event::TextDelta {
text: "live".into(),
},
)
.unwrap();
assert!(matches!(
next_client_frame(&mut stream).await,
protocol::Event::TextDelta { .. }
));
}
#[tokio::test]
async fn proxy_maps_runtime_worker_not_found_http_404_to_protocol_error_event() {
let (_runtime, _worker_ref, endpoint) = spawn_runtime_worker().await;
let endpoint = endpoint.replace("/protocol/ws", "/missing-worker/protocol/ws");
let source = RuntimeObservationSourceConfig {
runtime_id: "runtime-a".into(),
worker_id: "worker-a".into(),
endpoint,
bearer_token: None,
};
let (url, _dir) = spawn_workspace_proxy(source).await;
let (mut stream, _) = connect_async(&url).await.unwrap();
assert!(matches!(
next_client_frame(&mut stream).await,
protocol::Event::Error { .. }
));
}
async fn next_client_frame(
stream: &mut tokio_tungstenite::WebSocketStream<
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
>,
) -> protocol::Event {
let message = stream.next().await.unwrap().unwrap();
let Message::Text(text) = message else {
panic!("expected text frame");
};
serde_json::from_str(&text).unwrap()
}
async fn spawn_runtime_worker() -> (
worker_runtime::Runtime,
worker_runtime::identity::WorkerRef,
String,
) {
let (runtime, worker_ref) = runtime_with_worker();
let runtime_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let runtime_addr = runtime_listener.local_addr().unwrap();
tokio::spawn({
let runtime = runtime.clone();
async move {
worker_runtime::http_server::serve_runtime_http(runtime, runtime_listener, None)
.await
.unwrap()
}
});
let endpoint = format!(
"ws://{runtime_addr}/v1/workers/{}/protocol/ws",
worker_ref.worker_id
);
(runtime, worker_ref, endpoint)
}
async fn spawn_workspace_proxy(
source: RuntimeObservationSourceConfig,
) -> (String, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let store = SqliteWorkspaceStore::in_memory().unwrap();
let mut config = test_server_config(dir.path());
let runtime_id = source.runtime_id.clone();
let worker_id = source.worker_id.clone();
config.runtime_event_sources.push(source);
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(store),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
let app_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let app_addr = app_listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(app_listener, build_router(api)).await.unwrap() });
(
format!("ws://{app_addr}/api/runtimes/{runtime_id}/workers/{worker_id}/protocol/ws"),
dir,
)
}
#[tokio::test]
async fn workspace_subscription_uses_legacy_scoped_access_without_browser_session() {
let dir = tempfile::tempdir().unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let app = build_router(test_api(dir.path()).await);
let server = tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
});
let (mut socket, response) = tokio_tungstenite::connect_async(format!(
"ws://{address}/api/w/{TEST_WORKSPACE_ID}/protocol/ws"
))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::SWITCHING_PROTOCOLS);
socket.close(None).await.unwrap();
server.abort();
}
#[tokio::test]
async fn workspace_subscription_returns_workspace_snapshot() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
let spawn_request = WorkerSpawnRequest {
intent: WorkerSpawnIntent::WorkspaceCompanion,
requested_worker_name: Some("multiplexed-console".to_string()),
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 0,
},
profile: worker_runtime::catalog::ProfileSelector::Builtin(
"builtin:companion".to_string(),
),
ticket_assignment: None,
initial_input: None,
working_directory_request: None,
resolved_working_directory_request: None,
resolved_working_directory: None,
resolved_config_bundle: Some(runtime_test_bundle()),
resolved_workspace_api: None,
};
let spawned = api
.spawn_workspace_worker(EMBEDDED_WORKER_RUNTIME_ID, spawn_request)
.unwrap();
assert_eq!(spawned.state, WorkerOperationState::Accepted);
let worker_id = spawned.worker.unwrap().worker_id;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let app = build_router(api);
let server = tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
});
let (mut socket, _) = connect_async(format!(
"ws://{address}/api/w/{TEST_WORKSPACE_ID}/protocol/ws"
))
.await
.unwrap();
let frame = protocol::subscription::SubscriptionFrame::new(
protocol::subscription::SubscriptionFramePayload::Request(
protocol::subscription::SubscriptionRequest::SubscribeEvents {
request_id: protocol::subscription::SubscriptionRequestId::new("request-1")
.unwrap(),
selector: protocol::subscription::EventSubscriptionSelector::WorkspaceWorkers,
},
),
);
socket
.send(Message::Text(serde_json::to_string(&frame).unwrap().into()))
.await
.unwrap();
let Message::Text(text) = socket.next().await.unwrap().unwrap() else {
panic!("expected subscription response");
};
let response: protocol::subscription::SubscriptionFrame =
serde_json::from_str(text.as_str()).unwrap();
assert!(matches!(
response.payload,
protocol::subscription::SubscriptionFramePayload::Response(
protocol::subscription::SubscriptionResponse::Subscribed {
selector: protocol::subscription::EventSubscriptionSelector::WorkspaceWorkers,
snapshot: protocol::subscription::SubscriptionSnapshot::Workers { .. },
..
}
)
));
let subscribe_protocol = protocol::subscription::SubscriptionFrame::new(
protocol::subscription::SubscriptionFramePayload::Request(
protocol::subscription::SubscriptionRequest::SubscribeEvents {
request_id: protocol::subscription::SubscriptionRequestId::new("request-2")
.unwrap(),
selector: protocol::subscription::EventSubscriptionSelector::WorkerProtocol {
worker_id: protocol::subscription::SubscriptionWorkerId::new(
worker_id.clone(),
)
.unwrap(),
runtime_id: Some(EMBEDDED_WORKER_RUNTIME_ID.to_string()),
},
},
),
);
socket
.send(Message::Text(
serde_json::to_string(&subscribe_protocol).unwrap().into(),
))
.await
.unwrap();
let protocol_subscription_id = loop {
let Message::Text(text) = socket.next().await.unwrap().unwrap() else {
continue;
};
let frame: protocol::subscription::SubscriptionFrame =
serde_json::from_str(text.as_str()).unwrap();
if let protocol::subscription::SubscriptionFramePayload::Response(
protocol::subscription::SubscriptionResponse::Subscribed {
subscription_id,
selector:
protocol::subscription::EventSubscriptionSelector::WorkerProtocol { .. },
..
},
) = frame.payload
{
break subscription_id;
}
};
let second_subscribe = protocol::subscription::SubscriptionFrame::new(
protocol::subscription::SubscriptionFramePayload::Request(
protocol::subscription::SubscriptionRequest::SubscribeEvents {
request_id: protocol::subscription::SubscriptionRequestId::new("request-3")
.unwrap(),
selector: protocol::subscription::EventSubscriptionSelector::WorkerProtocol {
worker_id: protocol::subscription::SubscriptionWorkerId::new(
worker_id.clone(),
)
.unwrap(),
runtime_id: Some(EMBEDDED_WORKER_RUNTIME_ID.to_string()),
},
},
),
);
socket
.send(Message::Text(
serde_json::to_string(&second_subscribe).unwrap().into(),
))
.await
.unwrap();
let second_protocol_subscription_id = loop {
let Message::Text(text) = socket.next().await.unwrap().unwrap() else {
continue;
};
let frame: protocol::subscription::SubscriptionFrame =
serde_json::from_str(text.as_str()).unwrap();
if let protocol::subscription::SubscriptionFramePayload::Response(
protocol::subscription::SubscriptionResponse::Subscribed {
subscription_id,
selector:
protocol::subscription::EventSubscriptionSelector::WorkerProtocol { .. },
..
},
) = frame.payload
{
break subscription_id;
}
};
assert_ne!(protocol_subscription_id, second_protocol_subscription_id);
let unsubscribe = protocol::subscription::SubscriptionFrame::new(
protocol::subscription::SubscriptionFramePayload::Request(
protocol::subscription::SubscriptionRequest::UnsubscribeEvents {
request_id: protocol::subscription::SubscriptionRequestId::new("request-4")
.unwrap(),
subscription_id: protocol_subscription_id,
},
),
);
socket
.send(Message::Text(
serde_json::to_string(&unsubscribe).unwrap().into(),
))
.await
.unwrap();
let method = protocol::subscription::SubscriptionFrame::new(
protocol::subscription::SubscriptionFramePayload::WorkerProtocol(
protocol::subscription::SubscriptionWorkerProtocolMethod {
subscription_id: second_protocol_subscription_id.clone(),
method: protocol::Method::ListCompletions {
kind: protocol::CompletionKind::File,
prefix: String::new(),
},
},
),
);
socket
.send(Message::Text(
serde_json::to_string(&method).unwrap().into(),
))
.await
.unwrap();
loop {
let Message::Text(text) = socket.next().await.unwrap().unwrap() else {
continue;
};
let frame: protocol::subscription::SubscriptionFrame =
serde_json::from_str(text.as_str()).unwrap();
if matches!(
frame.payload,
protocol::subscription::SubscriptionFramePayload::Event(
protocol::subscription::SubscriptionEvent::Event {
subscription_id,
payload: protocol::subscription::SubscriptionEventPayload::WorkerProtocol {
event: protocol::Event::Completions { .. },
..
},
..
}
) if subscription_id == second_protocol_subscription_id
) {
break;
}
}
server.abort();
}
#[tokio::test]
async fn passkey_registration_rejects_unverified_credential_response() {
let dir = tempfile::tempdir().unwrap();
let app = test_app(dir.path()).await;
let registration_options = post_json(
app.clone(),
"/api/auth/passkeys/registration/options",
json!({
"handle": "alice",
"display_name": "Alice"
}),
)
.await;
assert!(registration_options["public_key"].is_object());
let challenge_id = registration_options["challenge_id"].as_str().unwrap();
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/passkeys/registration/complete")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&json!({
"challenge_id": challenge_id,
"credential": {
"id": "credential-1",
"rawId": "Y3JlZGVudGlhbC0x",
"type": "public-key",
"response": {
"clientDataJSON": "e30",
"attestationObject": "e30"
}
}
}))
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_ne!(response.status(), StatusCode::OK);
let login_without_registered_passkey = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/passkeys/login/options")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&json!({ "handle": "alice" })).unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_ne!(login_without_registered_passkey.status(), StatusCode::OK);
}
#[tokio::test]
async fn objective_mutation_endpoints_round_trip_through_workspace_authority() {
let dir = tempfile::tempdir().unwrap();
let config = test_server_config(dir.path());
let store = Arc::new(SqliteWorkspaceStore::open(&config.database_path).unwrap());
store
.upsert_workspace(&WorkspaceRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
owner_account_id: None,
display_name: "Test Workspace".to_string(),
state: "active".to_string(),
created_at: TEST_CREATED_AT.to_string(),
updated_at: TEST_CREATED_AT.to_string(),
})
.await
.unwrap();
let api = WorkspaceApi::new_with_execution_backend(
config,
store,
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
let app = build_router(api);
let objectives_path = format!("/api/w/{TEST_WORKSPACE_ID}/objectives");
let created = request_json(
app.clone(),
"POST",
&objectives_path,
Some(json!({
"title": "Objective CRUD",
"body_md": "First body",
"state": "active",
"linked_tickets": ["00000000001J2"]
})),
StatusCode::OK,
)
.await;
let id = created["id"].as_str().unwrap().to_string();
assert_eq!(created["title"], "Objective CRUD");
assert_eq!(created["linked_tickets"], json!(["00000000001J2"]));
let edited = request_json(
app.clone(),
"PATCH",
&format!("{objectives_path}/{id}"),
Some(json!({
"title": "Objective CRUD updated",
"old_string": "First",
"new_string": "Updated"
})),
StatusCode::OK,
)
.await;
assert_eq!(edited["title"], "Objective CRUD updated");
assert_eq!(edited["body"], "Updated body");
let state = request_json(
app.clone(),
"POST",
&format!("{objectives_path}/{id}/state"),
Some(json!({ "state": "paused" })),
StatusCode::OK,
)
.await;
assert_eq!(state["state"], "paused");
let linked = request_json(
app.clone(),
"POST",
&format!("{objectives_path}/{id}/ticket-links"),
Some(json!({ "ticket_id": "00000000001J3" })),
StatusCode::OK,
)
.await;
assert_eq!(
linked["linked_tickets"],
json!(["00000000001J2", "00000000001J3"])
);
let unlinked = request_json(
app.clone(),
"DELETE",
&format!("{objectives_path}/{id}/ticket-links/00000000001J2"),
None,
StatusCode::OK,
)
.await;
assert_eq!(unlinked["linked_tickets"], json!(["00000000001J3"]));
let shown = get_json(app, &format!("{objectives_path}/{id}")).await;
assert_eq!(shown["title"], "Objective CRUD updated");
assert_eq!(shown["state"], "paused");
assert_eq!(shown["linked_tickets"], json!(["00000000001J3"]));
}
async fn get_json(app: Router, uri: &str) -> Value {
let response = app
.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK, "{uri}");
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
serde_json::from_slice(&bytes).unwrap()
}
async fn request_json(
app: Router,
method: &str,
uri: &str,
body: Option<Value>,
expected_status: StatusCode,
) -> Value {
let mut builder = Request::builder().method(method).uri(uri);
let request_body = if let Some(body) = body {
builder = builder.header(CONTENT_TYPE, "application/json");
Body::from(serde_json::to_vec(&body).unwrap())
} else {
Body::empty()
};
let response = app
.oneshot(builder.body(request_body).unwrap())
.await
.unwrap();
let status = response.status();
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
assert_eq!(
status,
expected_status,
"{method} {uri}: {}",
String::from_utf8_lossy(&bytes)
);
serde_json::from_slice(&bytes).unwrap_or_else(
|_| serde_json::json!({ "message": String::from_utf8_lossy(&bytes).to_string() }),
)
}
async fn post_json(app: Router, uri: &str, body: Value) -> Value {
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(uri)
.header(CONTENT_TYPE, "application/json")
.body(Body::from(serde_json::to_vec(&body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK, "{uri}");
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
serde_json::from_slice(&bytes).unwrap()
}
fn write_ticket(
database_path: &Path,
workspace_id: &str,
title: &str,
state: ticket::TicketWorkflowState,
) {
use ticket::TicketBackend as _;
let backend = ticket::SqliteTicketBackend::new(database_path, workspace_id);
let mut input = ticket::NewTicket::new(title);
input.workflow_state = Some(state);
backend.create(input).unwrap();
}
fn write_objective(root: &Path, id: &str, title: &str, state: &str) {
let objective_dir = root.join(".yoi/objectives").join(id);
std::fs::create_dir_all(&objective_dir).unwrap();
std::fs::write(
objective_dir.join("item.md"),
format!(
r#"---
title: "{title}"
state: "{state}"
created_at: "2026-01-01T00:00:00Z"
updated_at: "2026-01-02T00:00:00Z"
linked_tickets: ["00000000001J2"]
---
Objective body.
"#,
),
)
.unwrap();
std::fs::write(
objective_dir.join("memory-architecture-overview.md"),
"# Memory architecture\n\nResource body.\n",
)
.unwrap();
}
}