pub mod auth;
pub mod commands;
pub mod dto;
pub mod executor;
pub mod projection;
pub mod proposal_subscriptions;
pub mod reads;
pub mod registry;
pub mod sinks;
pub mod stream;
pub mod worktrees;
use std::sync::Arc;
use axum::extract::{Request, State};
use axum::http::header;
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::Router;
use async_trait::async_trait;
use utoipa_swagger_ui::SwaggerUi;
use auth::{
cors_headers, is_preflight, preflight_response, resolve_correlation_id, CorrelationId,
RemoteControlAuth,
};
use dto::{CommandSpec, ErrorCode};
use executor::{CommandFailure, ExecutionSummary, RemoteControlExecutor};
use projection::Projection;
use worktrees::{UnboundWorktreeOperations, WorktreeListing, WorktreeOperations};
pub const HEALTH_PATH: &str = "/api/v2/health";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiTransport {
Unix,
Tcp,
}
#[utoipa::path(
get,
path = "/api/v2/openapi.yaml",
tag = "contract",
security(),
responses((status = 200, description = "OpenAPI document (YAML), content type `application/yaml`"))
)]
pub async fn openapi_yaml() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "application/yaml")],
crate::web::openapi::document_yaml(),
)
}
pub struct RemoteControlRuntime {
projection: Arc<Projection>,
executor: tokio::sync::RwLock<Option<Arc<dyn RemoteControlExecutor>>>,
worktrees: tokio::sync::RwLock<Option<Arc<dyn WorktreeOperations>>>,
gate: Arc<CommandGate>,
execution_facts: Arc<ExecutionFactsHandle>,
execution_contract: Arc<ExecutionContractHandle>,
completion_sinks: Arc<CompletionSinkHandle>,
}
#[derive(Default)]
pub struct CompletionSinkHandle {
registry: std::sync::RwLock<Option<Arc<crate::web::completion_sink::CompletionSinkRegistry>>>,
}
impl CompletionSinkHandle {
pub fn bind(&self, registry: Arc<crate::web::completion_sink::CompletionSinkRegistry>) {
*self
.registry
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(registry);
}
pub fn get(&self) -> Option<Arc<crate::web::completion_sink::CompletionSinkRegistry>> {
self.registry
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
}
#[derive(Default)]
pub struct ExecutionContractHandle {
contract: std::sync::RwLock<Option<dto::OwnerExecutionContract>>,
}
impl ExecutionContractHandle {
pub fn bind(&self, contract: dto::OwnerExecutionContract) {
*self
.contract
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(contract);
}
pub fn resolve(&self, change_id: Option<&str>) -> Option<dto::OwnerExecutionContract> {
let mut contract = self
.contract
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()?;
contract.pushed_branch = match (contract.terminal_mode, change_id) {
(dto::TerminalMode::BranchPushed, Some(change_id)) => Some(
crate::worktree_ops::service::branch_name_for_change(change_id),
),
_ => None,
};
Some(contract)
}
}
#[derive(Default)]
pub struct ExecutionFactsHandle {
facts:
std::sync::RwLock<Option<Arc<crate::orchestration::execution_facts::ExecutionFactsStore>>>,
boundary: std::sync::RwLock<
Option<Arc<dyn crate::orchestration::operator_command::RunBoundaryLiveness>>,
>,
}
impl ExecutionFactsHandle {
pub fn bind(&self, facts: Arc<crate::orchestration::execution_facts::ExecutionFactsStore>) {
*self
.facts
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(facts);
}
pub fn bind_boundary(
&self,
boundary: Arc<dyn crate::orchestration::operator_command::RunBoundaryLiveness>,
) {
*self
.boundary
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(boundary);
}
pub fn snapshot(&self) -> crate::orchestration::execution_facts::ExecutionFactsSnapshot {
match self
.facts
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.as_ref()
{
Some(facts) => facts.snapshot(),
None => Default::default(),
}
}
pub fn scheduler_running(&self) -> bool {
match self
.boundary
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.as_ref()
{
Some(boundary) => boundary.boundary_running(),
None => false,
}
}
}
#[derive(Default)]
pub struct CommandGate {
inner: tokio::sync::RwLock<Option<Arc<tokio::sync::Mutex<()>>>>,
}
impl CommandGate {
pub async fn bind(&self, gate: Arc<tokio::sync::Mutex<()>>) {
*self.inner.write().await = Some(gate);
}
pub async fn hold(&self) -> Option<tokio::sync::OwnedMutexGuard<()>> {
let gate = self.inner.read().await.clone()?;
Some(gate.lock_owned().await)
}
}
impl Default for RemoteControlRuntime {
fn default() -> Self {
Self::new()
}
}
impl RemoteControlRuntime {
pub fn new() -> Self {
Self {
projection: Arc::new(Projection::new()),
executor: tokio::sync::RwLock::new(None),
worktrees: tokio::sync::RwLock::new(None),
gate: Arc::new(CommandGate::default()),
execution_facts: Arc::new(ExecutionFactsHandle::default()),
execution_contract: Arc::new(ExecutionContractHandle::default()),
completion_sinks: Arc::new(CompletionSinkHandle::default()),
}
}
pub fn projection(&self) -> Arc<Projection> {
self.projection.clone()
}
pub fn gate(&self) -> Arc<CommandGate> {
self.gate.clone()
}
pub fn execution_facts(&self) -> Arc<ExecutionFactsHandle> {
self.execution_facts.clone()
}
pub fn execution_contract(&self) -> Arc<ExecutionContractHandle> {
self.execution_contract.clone()
}
pub fn completion_sinks(&self) -> Arc<CompletionSinkHandle> {
self.completion_sinks.clone()
}
pub fn bind_completion_sinks(
&self,
registry: Arc<crate::web::completion_sink::CompletionSinkRegistry>,
) {
self.completion_sinks.bind(registry);
}
pub fn bind_execution_contract(&self, contract: dto::OwnerExecutionContract) {
self.execution_contract.bind(contract);
}
pub fn bind_execution_facts(
&self,
facts: Arc<crate::orchestration::execution_facts::ExecutionFactsStore>,
) {
self.execution_facts.bind(facts);
}
pub fn bind_run_boundary(
&self,
boundary: Arc<dyn crate::orchestration::operator_command::RunBoundaryLiveness>,
) {
self.execution_facts.bind_boundary(boundary);
}
pub async fn bind(&self, executor: Arc<dyn RemoteControlExecutor>) {
*self.executor.write().await = Some(executor);
}
pub async fn bind_gate(&self, gate: Arc<tokio::sync::Mutex<()>>) {
self.gate.bind(gate).await;
}
pub async fn bind_worktrees(&self, worktrees: Arc<dyn WorktreeOperations>) {
*self.worktrees.write().await = Some(worktrees);
}
#[cfg_attr(not(test), allow(dead_code))]
pub async fn is_bound(&self) -> bool {
self.executor.read().await.is_some()
}
}
#[async_trait]
impl RemoteControlExecutor for RemoteControlRuntime {
async fn execute(&self, command: &CommandSpec) -> Result<ExecutionSummary, CommandFailure> {
let bound = self.executor.read().await.clone();
match bound {
Some(executor) => executor.execute(command).await,
None => Err(unbound_runtime()),
}
}
async fn begin(
&self,
command: &CommandSpec,
gate: Option<executor::GateGuard>,
) -> executor::Applied {
let bound = self.executor.read().await.clone();
match bound {
Some(executor) => executor.begin(command, gate).await,
None => executor::Applied::Settled(Err(unbound_runtime())),
}
}
async fn execute_held(
&self,
command: &CommandSpec,
gate: Option<executor::GateGuard>,
) -> Result<ExecutionSummary, CommandFailure> {
let bound = self.executor.read().await.clone();
match bound {
Some(executor) => executor.execute_held(command, gate).await,
None => Err(unbound_runtime()),
}
}
async fn is_command_capable(&self) -> bool {
self.executor.read().await.is_some()
}
}
fn unbound_runtime() -> CommandFailure {
CommandFailure::new(
ErrorCode::CommandExecutorUnbound,
"this instance has no orchestration runtime bound yet",
)
}
#[async_trait]
impl WorktreeOperations for RemoteControlRuntime {
async fn list(&self) -> Result<WorktreeListing, CommandFailure> {
match self.worktrees.read().await.clone() {
Some(port) => port.list().await,
None => UnboundWorktreeOperations.list().await,
}
}
async fn create(&self, change_id: &str) -> Result<ExecutionSummary, CommandFailure> {
match self.worktrees.read().await.clone() {
Some(port) => port.create(change_id).await,
None => UnboundWorktreeOperations.create(change_id).await,
}
}
async fn delete(&self, worktree_id: &str) -> Result<ExecutionSummary, CommandFailure> {
match self.worktrees.read().await.clone() {
Some(port) => port.delete(worktree_id).await,
None => UnboundWorktreeOperations.delete(worktree_id).await,
}
}
async fn merge(&self, worktree_id: &str) -> Result<ExecutionSummary, CommandFailure> {
match self.worktrees.read().await.clone() {
Some(port) => port.merge(worktree_id).await,
None => UnboundWorktreeOperations.merge(worktree_id).await,
}
}
}
#[derive(Clone)]
pub struct RemoteControlState {
pub projection: Arc<Projection>,
pub auth: Arc<RemoteControlAuth>,
pub executor: Arc<dyn RemoteControlExecutor>,
pub worktrees: Arc<dyn WorktreeOperations>,
pub gate: Arc<CommandGate>,
pub execution_facts: Arc<ExecutionFactsHandle>,
pub execution_contract: Arc<ExecutionContractHandle>,
pub completion_sinks: Arc<CompletionSinkHandle>,
}
impl RemoteControlState {
pub fn scheduler_running(&self) -> bool {
self.execution_facts.scheduler_running()
}
}
impl RemoteControlState {
pub fn new(
projection: Arc<Projection>,
auth: Arc<RemoteControlAuth>,
executor: Arc<dyn RemoteControlExecutor>,
) -> Self {
Self {
projection,
auth,
executor,
worktrees: Arc::new(UnboundWorktreeOperations),
gate: Arc::new(CommandGate::default()),
execution_facts: Arc::new(ExecutionFactsHandle::default()),
execution_contract: Arc::new(ExecutionContractHandle::default()),
completion_sinks: Arc::new(CompletionSinkHandle::default()),
}
}
pub fn with_completion_sinks(mut self, handle: Arc<CompletionSinkHandle>) -> Self {
self.completion_sinks = handle;
self
}
pub fn with_execution_contract(mut self, contract: Arc<ExecutionContractHandle>) -> Self {
self.execution_contract = contract;
self
}
pub fn with_gate(mut self, gate: Arc<CommandGate>) -> Self {
self.gate = gate;
self
}
pub fn with_execution_facts(mut self, facts: Arc<ExecutionFactsHandle>) -> Self {
self.execution_facts = facts;
self
}
pub fn with_worktrees(mut self, worktrees: Arc<dyn WorktreeOperations>) -> Self {
self.worktrees = worktrees;
self
}
}
pub fn router(state: RemoteControlState) -> Router {
Router::new()
.route(HEALTH_PATH, get(reads::health))
.route("/api/v2/capabilities", get(reads::capabilities))
.route("/api/v2/instance", get(reads::instance))
.route("/api/v2/state", get(reads::state))
.route("/api/v2/execution-status", get(reads::execution_status))
.route("/api/v2/execution-contract", get(reads::execution_contract))
.route(
"/api/v2/executions/{execution_id}/sink",
get(sinks::get_sink)
.put(sinks::put_sink)
.delete(sinks::delete_sink),
)
.route(
"/api/v2/proposals/{change_id}/subscription",
get(proposal_subscriptions::get_subscription)
.put(proposal_subscriptions::put_subscription)
.delete(proposal_subscriptions::delete_subscription),
)
.route("/api/v2/changes", get(reads::list_changes))
.route("/api/v2/changes/{change_id}", get(reads::get_change))
.route("/api/v2/logs", get(reads::logs))
.route("/api/v2/worktrees", get(reads::list_worktrees))
.route("/api/v2/worktrees/{worktree_id}", get(reads::get_worktree))
.route("/api/v2/commands", post(commands::submit_command))
.route("/api/v2/commands/{command_id}", get(commands::get_command))
.route("/api/v2/events", get(stream::events))
.route("/api/v2/ws", get(stream::ws))
.route("/api/v2/openapi.yaml", get(openapi_yaml))
.merge(
SwaggerUi::new("/api/v2/docs")
.url("/api/v2/openapi.json", crate::web::openapi::document()),
)
.route_layer(axum::middleware::from_fn_with_state(state.clone(), gate))
.with_state(state)
}
async fn gate(State(state): State<RemoteControlState>, request: Request, next: Next) -> Response {
let correlation = match resolve_correlation_id(request.headers()) {
Ok(id) => id,
Err(error) => return error.into_response(),
};
let allowed_origin = match state.auth.check_origin(request.headers(), &correlation) {
Ok(origin) => origin,
Err(error) => return error.into_response(),
};
if is_preflight(request.method()) {
return preflight_response(allowed_origin.as_deref());
}
if let Err(error) = state.auth.reject_out_of_band_credentials(
request.uri().query(),
request.headers(),
&correlation,
) {
return error.into_response();
}
if !crate::web::openapi::is_unauthenticated_v2_path(request.uri().path()) {
if let Err(error) = state.auth.check_bearer(request.headers(), &correlation) {
return error.into_response();
}
}
let mut request = request;
request
.extensions_mut()
.insert(CorrelationId(correlation.clone()));
let mut response = next.run(request).await;
for (name, value) in cors_headers(allowed_origin.as_deref()) {
response.headers_mut().insert(name, value);
}
if let Ok(value) = axum::http::HeaderValue::from_str(&correlation) {
response.headers_mut().insert("x-correlation-id", value);
}
response
}
#[cfg(test)]
mod tests;